Skip to content

Issue 53534: Support Microsoft's Graph Protocol API for authentication via OAuth2 token#7381

Open
labkey-bpatel wants to merge 32 commits intodevelopfrom
fb_mail_transport_via_graph
Open

Issue 53534: Support Microsoft's Graph Protocol API for authentication via OAuth2 token#7381
labkey-bpatel wants to merge 32 commits intodevelopfrom
fb_mail_transport_via_graph

Conversation

@labkey-bpatel
Copy link
Contributor

@labkey-bpatel labkey-bpatel commented Feb 3, 2026

Rationale

Microsoft plans to retire SMTP Basic Authentication in March 2026. In response, one of our clients’ IT department has requested a shift to OAuth-based authentication

Since SMTP Oauth 2.0 client credential flow with non-interactive sign-in (for email notifications) is not supported we’ll need to use Microsoft’s Graph API instead of SMTP protocol

This PR adds a Graph transport method as an alternative to SMTP, which enables mail delivery via OAuth2 authentication method.

Spec

Related Pull Requests

Changes

  • Add EmailTransportProvider interface.
  • Extract SMTP logic from MailHelper to its own provider class.
  • Add GraphTransportProvider that implements Graph API email transport using Microsoft SDK, handles attachments >3MB via upload sessions to accommodate Graph API's 4MB request limit.
  • Update MailHelper to manage multiple transport providers and detect configuration conflict (if both SMTP and Graph are configured)
  • Add EmailTestWithAttachmentAction in AdminController to test Graph API with HTML content, large attachments and data URI images.
  • Update emailTest.jsp to display success feedback after email send attempts and add Graph-specific test button for HTML and attachment workflow (button only visible when Graph is the active provider).
  • Add unit test to mock Microsoft Graph SDK to verify API interactions without real credentials.

Tasks 📍

  • Manual Testing
  • Unit test
  • Verify Fix

@github-actions
Copy link

github-actions bot commented Feb 3, 2026

WARNING: This PR appears to have the default title generated by GitHub. Please use something more descriptive.

@labkey-bpatel labkey-bpatel changed the title Fb mail transport via graph Issue 53534: Support Microsoft's Graph Protocol API for authentication via OAuth2 token Feb 3, 2026
*/
@AdminConsoleAction
@RequiresPermission(AdminOperationsPermission.class)
public class EmailTestWithAttachmentAction extends FormHandlerAction<EmailTestForm>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created this test action to test HTML content, attachments, and data URI images - scenarios that could hit Graph's size constraints. Should this remain separate for clarity, or would it be better to repurpose/extend EmailTestAction to test HTML/attachments for both transports?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have a strong opinion. If left as Graph API-specific, I would change the name to specify that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Name changed.

@labkey-bpatel labkey-bpatel marked this pull request as ready for review February 5, 2026 00:46
Copy link
Contributor

@labkey-adam labkey-adam left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code looks good, generally speaking. No required changes, but please review my questions & comments.

My biggest concern is the fact that this adds 70MB of JAR files to API. This is an extraordinary amount of new code for a single feature that will be used rarely. Were there any other options that would reduce the dependencies? Can any dependencies be excluded? If we can't slim it down, we may need to consider pushing the Graph API transport and dependencies into a separate module.

_configurationConflict = true;
String names = configured.stream()
.map(EmailTransportProvider::getName)
.collect(Collectors.joining(" and "));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not critical, but using StringUtils.joinWithConjunction() would possibly scale this better in the future. There are only two providers right now, but it's theoretically possible that we'd have > 2.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, now changed to use StringUtils.joinWithConjunction()

else
{
_session = DEFAULT_SESSION;
_activeProvider = _smtpProvider;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does calling this set _activeProvider? Only one provider can be configured, so either SMTP is already active or you'd be switching to an unconfigured provider. I could understand if this threw if SMTP is not active. This is also a concurrency risk since _activeProvider is not volatile. Ideally it would be final, set statically, and never changed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It shouldn't, now removed.

"<p>Sent via " + MailHelper.getActiveProvider().getName() + ".</p>" +
"</td></tr>" +
"</table>" +
"</body></html>");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this is "just" test HTML, but using DOM is preferred. I don't love that concatenated strings aren't HTML encoded here, even though it's just for testing. DOM would handle this automatically.

Copy link
Contributor Author

@labkey-bpatel labkey-bpatel Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely - I was focused on getting things working and missed adding the DOM layer for encoding. Now updated.

*/
@AdminConsoleAction
@RequiresPermission(AdminOperationsPermission.class)
public class EmailTestWithAttachmentAction extends FormHandlerAction<EmailTestForm>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have a strong opinion. If left as Graph API-specific, I would change the name to specify that.

}

return session;
public static void init()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you tried getting rid of init()? Seems unnecessary. The first caller who touches MailHelper should force the initialization to occur. I don't see any benefit of doing this proactively in ApiModule.doStartup().

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this initial code review, the design has changed. Removing init() may longer be necessary, please review the new changes.

@labkey-bpatel
Copy link
Contributor Author

labkey-bpatel commented Feb 24, 2026

Code looks good, generally speaking. No required changes, but please review my questions & comments.

My biggest concern is the fact that this adds 70MB of JAR files to API. This is an extraordinary amount of new code for a single feature that will be used rarely. Were there any other options that would reduce the dependencies? Can any dependencies be excluded? If we can't slim it down, we may need to consider pushing the Graph API transport and dependencies into a separate module.

We could trim ~10MB by cutting azure-identity, but that would require us to handle token management - not a significant saving. I am leaning toward pushing these changes to a standalone module for now, then we can revisit moving it into premiumModules down the road. Thoughts?

…er class to a new module in premiumModules. Rename test action to 'GraphEmailTestWithAttachmentAction'. Remove setting activeProvider in setSession. Use StringUtilsLabKey.joinWithConjunction()
Copy link
Contributor

@labkey-adam labkey-adam left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please see a few suggestions and questions. I'm approving and no further review is needed at this stage.

I do think we should consider some refactoring to make key classes less tied to SMTP and Jakarta Mail. MailHelper special cases SMTP in a few places, making some of the methods applicable to SMTP only. It's now odd that ViewMessage and MultipartMessage extend Jakarta Mail classes and hold an SMTP session, even when used with Graph. Ideally, MailHelper and the message classes would be completely generic to the provider. Dumbster could construct & configure an SmtpTransportProvider, and set that as MailHelper's active provider. However, I don't think any of this is required right now. I'd prefer to get this tested and merged, and get the client to test it. Then circle back with some improvements as a follow-on.

{
_session = DEFAULT_SESSION;
}
return _smtpProvider.getSession();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this return null if _smtpProvider is not configured?

public static Session getSession()
{
return _session;
return new MultipartMessage(getSession());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens here if getSession() returns null?

return session;
public static void setSession(Session session)
{
_smtpProvider.setSession(session);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this throw if called when Graph transport is active?

}

return session;
public static void setSession(Session session)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clarify method name? setSmtpSession()

* Returns the SMTP session for creating messages
*/
@Nullable
public static Session getSession()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clarify? getSmtpSession()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants