diff --git a/api/src/org/labkey/api/util/EmailTransportProvider.java b/api/src/org/labkey/api/util/EmailTransportProvider.java new file mode 100644 index 00000000000..1e80ae760a6 --- /dev/null +++ b/api/src/org/labkey/api/util/EmailTransportProvider.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2026 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.api.util; + +import jakarta.mail.Message; +import jakarta.mail.MessagingException; + +import java.util.Properties; + +/** + * Interface for email transport providers (SMTP, Microsoft Graph, etc.). + * Implementations handle configuration loading and message sending. + */ +public interface EmailTransportProvider +{ + /** + * @return the display name of this provider for logging purposes + */ + String getName(); + + /** + * Load configuration from startup properties and/or ServletContext. + * Called once during initialization. + */ + void loadConfiguration(); + + /** + * @return true if this provider is fully configured and ready to send email + */ + boolean isConfigured(); + + /** + * Send an email message using this transport. + * + * @param message the message to send + * @throws MessagingException if sending fails + */ + void send(Message message) throws MessagingException; + + /** + * @return the configuration properties for this provider + */ + Properties getProperties(); +} diff --git a/api/src/org/labkey/api/util/MailHelper.java b/api/src/org/labkey/api/util/MailHelper.java index b492d5e40ea..f528fb92c23 100644 --- a/api/src/org/labkey/api/util/MailHelper.java +++ b/api/src/org/labkey/api/util/MailHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2016 Fred Hutchinson Cancer Research Center + * Copyright (c) 2004-2026 Fred Hutchinson Cancer Research Center * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,24 +16,19 @@ package org.labkey.api.util; import jakarta.mail.Address; -import jakarta.mail.Authenticator; import jakarta.mail.BodyPart; import jakarta.mail.Message; import jakarta.mail.Message.RecipientType; import jakarta.mail.MessagingException; import jakarta.mail.Multipart; -import jakarta.mail.NoSuchProviderException; -import jakarta.mail.PasswordAuthentication; +import jakarta.mail.Part; import jakarta.mail.Session; -import jakarta.mail.Transport; import jakarta.mail.internet.AddressException; import jakarta.mail.internet.InternetAddress; import jakarta.mail.internet.MimeBodyPart; import jakarta.mail.internet.MimeMessage; import jakarta.mail.internet.MimeMultipart; -import jakarta.servlet.ServletContext; import org.apache.commons.lang3.StringUtils; -import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.Nullable; @@ -41,131 +36,110 @@ import org.labkey.api.audit.provider.MessageAuditProvider; import org.labkey.api.data.Container; import org.labkey.api.data.ContainerManager; -import org.labkey.api.module.ModuleLoader; import org.labkey.api.security.User; import org.labkey.api.settings.AppProps; -import org.labkey.api.settings.LenientStartupPropertyHandler; -import org.labkey.api.settings.StartupProperty; -import org.labkey.api.settings.StartupPropertyEntry; import org.labkey.api.util.emailTemplate.EmailTemplate; +import java.io.File; import java.io.IOException; import java.io.Writer; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Objects; -import java.util.Properties; import java.util.StringTokenizer; /** * Provides static functions for help with sending email. + * Supports SMTP and Microsoft Graph transport providers. */ public class MailHelper { public static final String MESSAGE_AUDIT_EVENT = "MessageAuditEvent"; private static final Logger _log = LogManager.getLogger(MailHelper.class); - private static final Session DEFAULT_SESSION; - private static Session _session = null; + // Transport providers + private static final SmtpTransportProvider _smtpProvider = new SmtpTransportProvider(); + private static final List _providers = new ArrayList<>(List.of(_smtpProvider)); - static - { - DEFAULT_SESSION = initDefaultSession(); - setSession(null); - } + // Active provider (set during initialization) + private static EmailTransportProvider _activeProvider = null; - public static void init() - { - // Invoked just to initialize DEFAULT_SESSION - } + // Configuration conflict flag + private static boolean _configurationConflict = false; - private static class SmtpStartupProperty implements StartupProperty + /** + * Load configuration for all providers and return the single configured provider. + * Sets _configurationConflict flag if multiple providers are configured. + * + * @return the configured provider, or null if none configured + */ + private static EmailTransportProvider loadActiveProvider() { - @Override - public String getPropertyName() + List configured = new ArrayList<>(); + + for (EmailTransportProvider provider : _providers) { - return ""; + provider.loadConfiguration(); + if (provider.isConfigured()) + { + configured.add(provider); + } } - @Override - public String getDescription() + if (configured.size() > 1) { - return "One property for each JavaMail SMTP setting, documented here: https://javaee.github.io/javamail/docs/api/com/sun/mail/smtp/package-summary.html"; + _configurationConflict = true; + String names = StringUtilsLabKey.joinWithConjunction(configured.stream() + .map(EmailTransportProvider::getName) + .toList(), "and"); + _log.error("Invalid email configuration: {} are configured. " + + "Please configure only one email transport method. Email sending will fail until this is resolved.", names); + return null; } + + return configured.isEmpty() ? null : configured.get(0); } - private static Session initDefaultSession() + public static EmailTransportProvider getActiveProvider() { - Session session = null; - try - { - /* first check if specified in startup properties */ - var properties = new Properties(); - ModuleLoader.getInstance().handleStartupProperties(new LenientStartupPropertyHandler<>("mail_smtp", new SmtpStartupProperty()) - { - @Override - public void handle(Collection entries) - { - entries.forEach(entry -> properties.put("mail.smtp." + entry.getName(), entry.getValue())); - } - }); + return _activeProvider; + } - /* now check if specified in tomcat config instead */ - if (properties.isEmpty()) - { - ServletContext context = ModuleLoader.getServletContext(); - Enumeration names = Objects.requireNonNull(context).getInitParameterNames(); - while (names.hasMoreElements()) - { - String name = names.nextElement(); - if (name.startsWith("mail.smtp.")) - properties.put(name, context.getInitParameter(name)); - } - } + public static boolean hasActiveProvider() + { + return null != _activeProvider; + } - if (!properties.isEmpty()) - { - session = Session.getInstance(properties); + /** + * Registers an optional transport provider. Must be called during module {@code init()} so that + * all providers are in place before {@link #init()} calls {@link #loadActiveProvider()}. + */ + public static void registerProvider(EmailTransportProvider provider) + { + _providers.add(provider); + } - if ("true".equalsIgnoreCase(session.getProperty("mail.smtp.ssl.enable")) || - "true".equalsIgnoreCase(session.getProperty("mail.smtp.starttls.enable"))) - { - String username = session.getProperty("mail.smtp.user"); - String password = session.getProperty("mail.smtp.password"); - session = Session.getInstance(session.getProperties(), new Authenticator() { - @Override - protected PasswordAuthentication getPasswordAuthentication() - { - return new PasswordAuthentication(username, password); - } - }); - } - } - } - catch (Exception e) - { - _log.log(Level.ERROR, "Exception loading mail session", e); - } + public static void init() + { + _activeProvider = loadActiveProvider(); + } - return session; + public static void setSession(Session session) + { + _smtpProvider.setSession(session); } - public static void setSession(@Nullable Session session) + /** + * Returns the SMTP session for creating messages + */ + @Nullable + public static Session getSession() { - if (session != null) - { - _session = session; - } - else - { - _session = DEFAULT_SESSION; - } + return _smtpProvider.getSession(); } /** @@ -173,20 +147,12 @@ public static void setSession(@Nullable Session session) */ public static ViewMessage createMessage() { - return new ViewMessage(_session); + return new ViewMessage(getSession()); } public static MultipartMessage createMultipartMessage() { - return new MultipartMessage(_session); - } - - /** - * Returns the session that will be used for all messages - */ - public static Session getSession() - { - return _session; + return new MultipartMessage(getSession()); } /** @@ -222,9 +188,10 @@ public static Address[] createAddressArray(String s) throws AddressException } /** - * Sends an email message, using the system mail session, and SMTP transport. This function logs a warning on a - * MessagingException, and then throws it to the caller. The caller should avoid double-logging the failure, but - * may want to handle the exception in some other way, e.g. displaying a message to the user. + * Sends an email message using the configured transport provider. This method logs + * exceptions before throwing them to the caller. The caller should avoid double-logging + * the failure but may want to handle the exception in some other way, e.g., displaying + * a message to the user. * * @param m the message to send * @param user for auditing purposes, the user who originated the message @@ -234,14 +201,26 @@ public static void send(Message m, @Nullable User user, Container c) { try { - Transport.send(m); + // Check for configuration conflict + if (_configurationConflict) + { + throw new ConfigurationException( + "Multiple email transport methods are configured. Please configure only one email transport method."); + } + + // Check if any provider is configured + if (_activeProvider == null) + { + throw new ConfigurationException( + "No email transport configured. Please configure either SMTP (mail.smtp.*) " + + "or Microsoft Graph (mail.graph.*) settings."); + } + + // Send via the active provider + _activeProvider.send(m); addAuditEvent(user, c, m); } - catch (NoSuchProviderException e) - { - _log.log(Level.ERROR, "Error getting SMTP transport"); - } - catch (NumberFormatException | MessagingException e) + catch (MessagingException e) { logMessagingException(m, e); throw new ConfigurationException("Error sending email: " + e.getMessage(), e); @@ -281,13 +260,13 @@ private static String getAddressStr(Address[] addresses) { sb.append(sep); sb.append(a.toString()); - sep = ", "; } return sb.toString(); } - private static final String ERROR_MESSAGE = "Exception sending email; check your SMTP configuration in " + AppProps.getInstance().getWebappConfigurationFilename(); + private static final String ERROR_MESSAGE = "Exception sending email; check your email configuration in " + + AppProps.getInstance().getWebappConfigurationFilename(); private static void logMessagingException(Message m, Exception e) { @@ -304,7 +283,6 @@ private static void logMessagingException(Message m, Exception e) } } - public static void renderHtml(Message m, String title, Writer out) { try @@ -442,6 +420,41 @@ public void setEncodedHtmlContent(String encodedHtml) throws MessagingException setBodyContent(encodedHtml, "text/html; charset=UTF-8"); } + public void addAttachment(File file) throws MessagingException, IOException + { + Object content; + try + { + content = getContent(); + } + catch (Exception e) + { + content = null; + } + + MimeMultipart mixed; + if (content instanceof MimeMultipart existingMultipart) + { + // Wrap existing content in a mixed multipart + mixed = new MimeMultipart("mixed"); + MimeBodyPart existingPart = new MimeBodyPart(); + existingPart.setContent(existingMultipart); + mixed.addBodyPart(existingPart); + setContent(mixed); + } + else + { + mixed = new MimeMultipart("mixed"); + setContent(mixed); + } + + MimeBodyPart attachment = new MimeBodyPart(); + attachment.setDataHandler(new jakarta.activation.DataHandler(new jakarta.activation.FileDataSource(file))); + attachment.setFileName(file.getName()); + attachment.setDisposition(Part.ATTACHMENT); + mixed.addBodyPart(attachment); + } + public void setTemplate(EmailTemplate template, Container c) throws MessagingException { String body = template.renderBody(c); @@ -451,7 +464,6 @@ public void setTemplate(EmailTemplate template, Container c) throws MessagingExc } } - /** * Sends one or more email messages in a background thread. Add message(s) to the emailer, then call start(). */ diff --git a/api/src/org/labkey/api/util/SmtpTransportProvider.java b/api/src/org/labkey/api/util/SmtpTransportProvider.java new file mode 100644 index 00000000000..5825b583aa4 --- /dev/null +++ b/api/src/org/labkey/api/util/SmtpTransportProvider.java @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2026 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.api.util; + +import jakarta.mail.Authenticator; +import jakarta.mail.Message; +import jakarta.mail.MessagingException; +import jakarta.mail.PasswordAuthentication; +import jakarta.mail.Session; +import jakarta.mail.Transport; +import jakarta.servlet.ServletContext; +import org.apache.logging.log4j.Logger; +import org.labkey.api.module.ModuleLoader; +import org.labkey.api.settings.LenientStartupPropertyHandler; +import org.labkey.api.settings.StartupProperty; +import org.labkey.api.settings.StartupPropertyEntry; +import org.labkey.api.util.logging.LogHelper; + +import java.util.Collection; +import java.util.Enumeration; +import java.util.Objects; +import java.util.Properties; + +/** + * SMTP email transport provider. + * Loads SMTP configuration and sends email via JavaMail Transport. + */ +public class SmtpTransportProvider implements EmailTransportProvider +{ + private static final Logger LOG = LogHelper.getLogger(SmtpTransportProvider.class, "SMTP Transport Provider"); + + private final Properties _properties = new Properties(); + private Session _session = null; + + private static class SmtpStartupProperty implements StartupProperty + { + @Override + public String getPropertyName() + { + return ""; + } + + @Override + public String getDescription() + { + return "One property for each JavaMail SMTP setting, documented here: https://javaee.github.io/javamail/docs/api/com/sun/mail/smtp/package-summary.html"; + } + } + + @Override + public String getName() + { + return "SMTP"; + } + + @Override + public void loadConfiguration() + { + try + { + // Load from startup properties group "mail_smtp" + ModuleLoader.getInstance().handleStartupProperties( + new LenientStartupPropertyHandler<>("mail_smtp", new SmtpStartupProperty()) + { + @Override + public void handle(Collection entries) + { + entries.forEach(entry -> + _properties.put("mail.smtp." + entry.getName(), entry.getValue())); + } + }); + + // Fallback: check ServletContext for SMTP settings + if (_properties.isEmpty()) + { + ServletContext context = ModuleLoader.getServletContext(); + Enumeration names = Objects.requireNonNull(context).getInitParameterNames(); + while (names.hasMoreElements()) + { + String name = names.nextElement(); + if (name.startsWith("mail.smtp.")) + _properties.put(name, context.getInitParameter(name)); + } + } + + // Create session if configured + if (isConfigured()) + { + _session = Session.getInstance(_properties); + + if ("true".equalsIgnoreCase(_session.getProperty("mail.smtp.ssl.enable")) || + "true".equalsIgnoreCase(_session.getProperty("mail.smtp.starttls.enable"))) + { + String username = _session.getProperty("mail.smtp.user"); + String password = _session.getProperty("mail.smtp.password"); + _session = Session.getInstance(_session.getProperties(), new Authenticator() + { + @Override + protected PasswordAuthentication getPasswordAuthentication() + { + return new PasswordAuthentication(username, password); + } + }); + } + LOG.info("Email configured to use SMTP transport"); + } + } + catch (Exception e) + { + LOG.error("Exception loading SMTP configuration", e); + } + } + + @Override + public boolean isConfigured() + { + return !_properties.isEmpty() && _properties.getProperty("mail.smtp.host") != null; + } + + @Override + public void send(Message message) throws MessagingException + { + if (_session == null) + { + throw new MessagingException("SMTP session not initialized"); + } + Transport.send(message); + } + + /** + * @return the SMTP session for creating messages, or null if not configured + */ + public Session getSession() + { + return _session; + } + + public void setSession(Session session) + { + _session = session; + } + + @Override + public Properties getProperties() + { + return _properties; + } +} diff --git a/core/src/org/labkey/core/admin/AdminController.java b/core/src/org/labkey/core/admin/AdminController.java index aa0688f7789..b1a0a54617f 100644 --- a/core/src/org/labkey/core/admin/AdminController.java +++ b/core/src/org/labkey/core/admin/AdminController.java @@ -22,6 +22,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Joiner; import com.google.common.util.concurrent.UncheckedExecutionException; +import jakarta.mail.Message; import jakarta.mail.MessagingException; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -411,12 +412,14 @@ import static org.labkey.api.util.DOM.Attribute.href; import static org.labkey.api.util.DOM.Attribute.method; import static org.labkey.api.util.DOM.Attribute.name; +import static org.labkey.api.util.DOM.Attribute.src; import static org.labkey.api.util.DOM.Attribute.style; import static org.labkey.api.util.DOM.Attribute.title; import static org.labkey.api.util.DOM.Attribute.type; import static org.labkey.api.util.DOM.Attribute.value; import static org.labkey.api.util.DOM.BR; import static org.labkey.api.util.DOM.DIV; +import static org.labkey.api.util.DOM.IMG; import static org.labkey.api.util.DOM.LI; import static org.labkey.api.util.DOM.SPAN; import static org.labkey.api.util.DOM.STYLE; @@ -8549,6 +8552,28 @@ public static class EmailTestForm private String _to; private String _body; private ConfigurationException _exception; + private boolean _success; + private boolean _attachmentSuccess; + + public boolean isSuccess() + { + return _success; + } + + public void setSuccess(boolean success) + { + _success = success; + } + + public boolean isAttachmentSuccess() + { + return _attachmentSuccess; + } + + public void setAttachmentSuccess(boolean attachmentSuccess) + { + _attachmentSuccess = attachmentSuccess; + } public String getTo() { @@ -8591,6 +8616,8 @@ public String getFrom(Container c) @RequiresPermission(AdminOperationsPermission.class) public class EmailTestAction extends FormViewAction { + private static final String EMAIL_TEST_SUCCESS_KEY = "EmailTestAction.success"; + @Override public void validateCommand(EmailTestForm form, Errors errors) { @@ -8615,10 +8642,28 @@ public void validateCommand(EmailTestForm form, Errors errors) @Override public ModelAndView getView(EmailTestForm form, boolean reshow, BindException errors) { + // Check for flash message from previous successful send + if (Boolean.TRUE.equals(getViewContext().getSession().getAttribute(EMAIL_TEST_SUCCESS_KEY))) + { + getViewContext().getSession().removeAttribute(EMAIL_TEST_SUCCESS_KEY); + form.setSuccess(true); + } + if (Boolean.TRUE.equals(getViewContext().getSession().getAttribute(GraphEmailTestWithAttachmentAction.EMAIL_TEST_ATTACHMENT_SUCCESS_KEY))) + { + getViewContext().getSession().removeAttribute(GraphEmailTestWithAttachmentAction.EMAIL_TEST_ATTACHMENT_SUCCESS_KEY); + form.setAttachmentSuccess(true); + } + String attachmentError = (String) getViewContext().getSession().getAttribute(GraphEmailTestWithAttachmentAction.EMAIL_TEST_ATTACHMENT_ERROR_KEY); + if (attachmentError != null) + { + getViewContext().getSession().removeAttribute(GraphEmailTestWithAttachmentAction.EMAIL_TEST_ATTACHMENT_ERROR_KEY); + form.setException(new ConfigurationException(attachmentError)); + } + JspView testView = new JspView<>("/org/labkey/core/admin/emailTest.jsp", form); testView.setTitle("Send a Test Email"); - if(null != MailHelper.getSession() && null != MailHelper.getSession().getProperties()) + if(MailHelper.hasActiveProvider()) { JspView emailPropsView = new JspView<>("/org/labkey/core/admin/emailProps.jsp"); emailPropsView.setTitle("Current Email Settings"); @@ -8647,6 +8692,7 @@ public boolean handlePost(EmailTestForm form, BindException errors) throws Excep try { MailHelper.send(msg, getUser(), getContainer()); + getViewContext().getSession().setAttribute(EMAIL_TEST_SUCCESS_KEY, Boolean.TRUE); } catch (ConfigurationException e) { @@ -8680,6 +8726,110 @@ public void addNavTrail(NavTree root) } } + /** + * Tests the Microsoft Graph API email transport with HTML content and a large attachment. + * This action tests the Graph API upload session workflow for attachments over 3MB, + * and data URI to CID attachment conversion (embedded base64 images in HTML). + * Use {@link EmailTestAction} for general email testing for both SMTP and Graph API. + */ + @AdminConsoleAction + @RequiresPermission(AdminOperationsPermission.class) + public class GraphEmailTestWithAttachmentAction extends FormHandlerAction + { + private static final String EMAIL_TEST_ATTACHMENT_SUCCESS_KEY = "GraphEmailTestWithAttachmentAction.success"; + private static final String EMAIL_TEST_ATTACHMENT_ERROR_KEY = "GraphEmailTestWithAttachmentAction.error"; + + @Override + public void validateCommand(EmailTestForm form, Errors errors) + { + if (form.getTo() == null || form.getTo().isEmpty()) + { + errors.reject(ERROR_MSG, "To field cannot be blank."); + } + } + + @Override + public boolean handlePost(EmailTestForm form, BindException errors) throws Exception + { + if (errors.hasErrors()) + return false; + + if (!MailHelper.hasActiveProvider()) + { + form.setException(new ConfigurationException("No email provider configured")); + return false; + } + + ValidEmail recipient = new ValidEmail(form.getTo()); + + // Create a temp file for attachment, deleting any leftover from previous test + File tempFile = FileUtil.appendPath(FileUtil.getTempDirectory(), org.labkey.api.util.Path.parse("email_test_attachment.txt")); + FileUtil.deleteTempFile(tempFile); + + // Generate ~4MB of content to test Graph API upload session chunked upload + StringBuilder sb = new StringBuilder(); + sb.append("LabKey Server Email Attachment Test\n"); + sb.append("Provider: ").append(MailHelper.getActiveProvider().getName()).append("\n"); + sb.append("Timestamp: ").append(java.time.Instant.now()).append("\n\n"); + String line = "This is test data for email attachment upload testing. "; + while (sb.length() < 4 * 1024 * 1024) + { + sb.append(line); + } + java.nio.file.Files.writeString(tempFile.toPath(), sb.toString());//creates the actual file and writes the content + + try + { + // Use the Graph provider's configured from address + String fromAddress = MailHelper.getActiveProvider().getProperties().getProperty("mail.graph.fromAddress"); + MailHelper.MultipartMessage msg = MailHelper.createMultipartMessage(); + msg.setFrom(fromAddress); + msg.addRecipient(Message.RecipientType.TO, recipient.getAddress()); + msg.setSubject("Test Email with HTML and Attachment"); + + // Test both external URL images and data URI images. + // External URLs should be left unchanged, while data URIs should be converted to CID attachments. + + // External image URL served by the running LabKey server (should NOT be converted - left as-is) + String logoUrl = "https://www.labkey.org/_webdav/Documentation/%40files/badge.png"; + + // Data URI image built from actual webapp image (should be converted to CID attachment by GraphTransportProvider) + FileLike imagesDir = ModuleLoader.getInstance().getModule("Core").getStaticFileDirectories().stream() + .map(dir -> dir.resolveChild("_images")) + .filter(FileLike::isDirectory) + .findFirst() + .orElseThrow(() -> new ConfigurationException("Could not find _images directory in core module")); + String gifDataUri = "data:image/gif;base64," + java.util.Base64.getEncoder().encodeToString(java.nio.file.Files.readAllBytes(imagesDir.resolveChild("paperclip.gif").toNioPathForRead())); + + msg.setEncodedHtmlContent(createHtmlFragment( + DIV("This is a ", SPAN(at(style, "font-weight:bold"), "test email"), " with HTML content, attachment, and multiple images."), + DIV(SPAN(at(style, "font-weight:bold"), "External URL image"), " (should remain unchanged):"), + IMG(at(src, logoUrl, style, "height:30px")), + DIV(SPAN(at(style, "font-weight:bold"), "Data URI image"), " (should be converted to CID attachment):"), + IMG(at(src, gifDataUri, style, "height:30px")), + DIV("Sent via ", MailHelper.getActiveProvider().getName(), ".") + ).toString()); + msg.addAttachment(tempFile); + + MailHelper.send(msg, getUser(), getContainer()); + getViewContext().getSession().setAttribute(EMAIL_TEST_ATTACHMENT_SUCCESS_KEY, Boolean.TRUE); + } + catch (Exception e) + { + getViewContext().getSession().setAttribute(EMAIL_TEST_ATTACHMENT_ERROR_KEY, e.getMessage()); + return true; + } + + return true; + } + + @Override + public URLHelper getSuccessURL(EmailTestForm form) + { + return new ActionURL(EmailTestAction.class, getContainer()); + } + } + @RequiresPermission(AdminOperationsPermission.class) public static class RecreateViewsAction extends ConfirmAction { diff --git a/core/src/org/labkey/core/admin/emailProps.jsp b/core/src/org/labkey/core/admin/emailProps.jsp index 10dffc1af32..f3035dd71d3 100644 --- a/core/src/org/labkey/core/admin/emailProps.jsp +++ b/core/src/org/labkey/core/admin/emailProps.jsp @@ -1,6 +1,6 @@ <% /* - * Copyright (c) 2008-2010 LabKey Corporation + * Copyright (c) 2008-2026 LabKey Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,19 +16,27 @@ */ %> <%@ page import="org.labkey.api.util.MailHelper" %> +<%@ page import="org.labkey.api.util.EmailTransportProvider" %> <%@ page import="java.util.Properties" %> <%@ page import="java.util.Set" %> <%@ page import="org.labkey.api.collections.CaseInsensitiveHashSet" %> <%@ page extends="org.labkey.api.jsp.JspBase" %> <% - Properties emailProps = MailHelper.getSession().getProperties(); + EmailTransportProvider activeProvider = MailHelper.getActiveProvider(); + Properties emailProps = activeProvider != null ? activeProvider.getProperties() : new Properties(); + String providerName = activeProvider != null ? activeProvider.getName() : "None"; Set obscuredProps = new CaseInsensitiveHashSet( "mail.smtp.user", - "mail.smtp.password" + "mail.smtp.password", + "mail.graph.clientSecret" ); %> + + + + <% for(Object key : emailProps.keySet()) { %> diff --git a/core/src/org/labkey/core/admin/emailTest.jsp b/core/src/org/labkey/core/admin/emailTest.jsp index 9994760e877..e2160730b87 100644 --- a/core/src/org/labkey/core/admin/emailTest.jsp +++ b/core/src/org/labkey/core/admin/emailTest.jsp @@ -20,8 +20,10 @@ <%@ page import="org.labkey.api.util.PageFlowUtil" %> <%@ page import="org.labkey.api.view.HttpView" %> <%@ page import="org.labkey.api.view.JspView" %> +<%@ page import="org.labkey.api.util.MailHelper" %> <%@ page import="org.labkey.core.admin.AdminController.EmailTestAction" %> <%@ page import="org.labkey.core.admin.AdminController.EmailTestForm" %> +<%@ page import="org.labkey.core.admin.AdminController.GraphEmailTestWithAttachmentAction" %> <%@ taglib prefix="labkey" uri="http://www.labkey.org/taglib" %> <%@ page extends="org.labkey.api.jsp.JspBase" %> <% @@ -33,7 +35,19 @@ message to the address specified in the 'To' text box containing the content specified in the 'Body' text box.

-<% if (null != form.getException()) { %> +<% if (form.isSuccess()) { %> + +<% } else if (form.isAttachmentSuccess()) { %> + +<% } else if (null != form.getException()) { %>
Your message could not be sent for the following reason(s):
<%=h(form.getException().getMessage())%>
@@ -58,4 +72,22 @@ message to the address specified in the 'To' text box containing the content spe
Transport<%=h(providerName)%>
<%=h(key.toString())%>
- \ No newline at end of file + + +<% if (MailHelper.getActiveProvider() != null && "Microsoft Graph".equals(MailHelper.getActiveProvider().getName())) { %> +
+

Microsoft Graph Attachment Test: Send a test email with HTML content and a 4MB file attachment +to verify the Graph API upload session workflow.

+ + + + + + + +
+ <%= button("Send HTML + Attachment Test").submit(true).onClick("document.getElementById('attachmentTestBtn').style.display='none'; document.getElementById('attachmentTestSending').style.display='inline';") %> + +
+
+<% } %> \ No newline at end of file