diff --git a/commons-email2-core/src/main/java/org/apache/commons/mail2/core/EmailConstants.java b/commons-email2-core/src/main/java/org/apache/commons/mail2/core/EmailConstants.java index d2d816064..b4210efef 100644 --- a/commons-email2-core/src/main/java/org/apache/commons/mail2/core/EmailConstants.java +++ b/commons-email2-core/src/main/java/org/apache/commons/mail2/core/EmailConstants.java @@ -56,6 +56,9 @@ public final class EmailConstants { /** If set to true, tries to authenticate the user using the AUTH command. */ public static final String MAIL_SMTP_AUTH = "mail.smtp.auth"; + /** If set to true, tries to authenticate the user using an OAuth2 token. */ + public static final String MAIL_SMTP_AUTH_MECHANISMS = "mail.smtp.auth.mechanisms"; + /** The SMTP user name. */ public static final String MAIL_SMTP_USER = "mail.smtp.user"; diff --git a/commons-email2-jakarta/src/main/java/org/apache/commons/mail2/jakarta/Email.java b/commons-email2-jakarta/src/main/java/org/apache/commons/mail2/jakarta/Email.java index f4f67b57b..9c54fb0f7 100644 --- a/commons-email2-jakarta/src/main/java/org/apache/commons/mail2/jakarta/Email.java +++ b/commons-email2-jakarta/src/main/java/org/apache/commons/mail2/jakarta/Email.java @@ -214,6 +214,11 @@ public abstract class Email { */ private boolean startTlsRequired; + /** + * If true, uses OAuth2 token for authentication. + */ + private boolean oauth2Required; + /** * Does the current transport use SSL/TLS encryption upon connection? */ @@ -823,6 +828,10 @@ public Session getMailSession() throws EmailException { properties.setProperty(EmailConstants.MAIL_SMTP_AUTH, "true"); } + if (isOAuth2Required()) { + properties.put(EmailConstants.MAIL_SMTP_AUTH_MECHANISMS, "XOAUTH2"); + } + if (isSSLOnConnect()) { properties.setProperty(EmailConstants.MAIL_PORT, sslSmtpPort); properties.setProperty(EmailConstants.MAIL_SMTP_SOCKET_FACTORY_PORT, sslSmtpPort); @@ -1062,6 +1071,16 @@ public boolean isStartTLSRequired() { return startTlsRequired; } + /** + * Tests whether the client is configured to use OAuth2 authentication. + * + * @return true if using OAuth2 for authentication, false otherwise. + * @since 2.0 + */ + public boolean isOAuth2Required() { + return oauth2Required; + } + /** * Sends the email. Internally we build a MimeMessage which is afterwards sent to the SMTP server. * @@ -1640,6 +1659,20 @@ public Email setStartTLSRequired(final boolean startTlsRequired) { return this; } + /** + * Sets or disables OAuth2 authentication. + * + * @param oauth2Required true if OAUth2 authentication is required, false otherwise + * @return An Email. + * @throws IllegalStateException if the mail session is already initialized + * @since 2.0 + */ + public Email setOAuth2Required(final boolean oauth2Required) { + checkSessionAlreadyInitialized(); + this.oauth2Required = oauth2Required; + return this; + } + /** * Sets the email subject. Replaces end-of-line characters with spaces. * diff --git a/commons-email2-jakarta/src/test/java/org/apache/commons/mail2/jakarta/EmailTest.java b/commons-email2-jakarta/src/test/java/org/apache/commons/mail2/jakarta/EmailTest.java index 0ce1bb622..e734b0a62 100644 --- a/commons-email2-jakarta/src/test/java/org/apache/commons/mail2/jakarta/EmailTest.java +++ b/commons-email2-jakarta/src/test/java/org/apache/commons/mail2/jakarta/EmailTest.java @@ -19,6 +19,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -571,6 +572,17 @@ public void testGetSetAuthentication() { assertEquals(strPassword, retrievedAuth.getPasswordAuthentication().getPassword()); } + @Test + public void testSetOAuth2Required() throws EmailException { + email.setHostName(strTestMailServer); + email.setOAuth2Required(true); + final Session mailSession = email.getMailSession(); + + // tests + assertNotNull(mailSession); + assertEquals("XOAUTH2", mailSession.getProperties().getProperty("mail.smtp.auth.mechanisms")); + } + @Test public void testGetSetAuthenticator() { // setup diff --git a/commons-email2-javax/src/main/java/org/apache/commons/mail2/javax/Email.java b/commons-email2-javax/src/main/java/org/apache/commons/mail2/javax/Email.java index 1bea918c0..8150deff0 100644 --- a/commons-email2-javax/src/main/java/org/apache/commons/mail2/javax/Email.java +++ b/commons-email2-javax/src/main/java/org/apache/commons/mail2/javax/Email.java @@ -213,6 +213,11 @@ public abstract class Email { */ private boolean startTlsRequired; + /** + * If true, uses OAuth2 for authentication. + */ + private boolean oauth2Required; + /** * Does the current transport use SSL/TLS encryption upon connection? */ @@ -822,6 +827,10 @@ public Session getMailSession() throws EmailException { properties.setProperty(EmailConstants.MAIL_SMTP_AUTH, "true"); } + if (isOAuth2Required()) { + properties.put(EmailConstants.MAIL_SMTP_AUTH_MECHANISMS, "XOAUTH2"); + } + if (isSSLOnConnect()) { properties.setProperty(EmailConstants.MAIL_PORT, sslSmtpPort); properties.setProperty(EmailConstants.MAIL_SMTP_SOCKET_FACTORY_PORT, sslSmtpPort); @@ -1061,6 +1070,16 @@ public boolean isStartTLSRequired() { return startTlsRequired; } + /** + * Tests whether the client is configured to use OAuth2 authentication. + * + * @return true if using OAuth2 for authentication, false otherwise. + * @since 2.0 + */ + public boolean isOAuth2Required() { + return oauth2Required; + } + /** * Sends the email. Internally we build a MimeMessage which is afterwards sent to the SMTP server. * @@ -1639,6 +1658,20 @@ public Email setStartTLSRequired(final boolean startTlsRequired) { return this; } + /** + * Sets or disables OAuth2 authentication. + * + * @param oauth2Required true if OAUth2 authentication is required, false otherwise + * @return An Email. + * @throws IllegalStateException if the mail session is already initialized + * @since 2.0 + */ + public Email setOAuth2Required(final boolean oauth2Required) { + checkSessionAlreadyInitialized(); + this.oauth2Required = oauth2Required; + return this; + } + /** * Sets the email subject. Replaces end-of-line characters with spaces. * diff --git a/commons-email2-javax/src/test/java/org/apache/commons/mail2/javax/EmailTest.java b/commons-email2-javax/src/test/java/org/apache/commons/mail2/javax/EmailTest.java index 0e7af00be..b6940f704 100644 --- a/commons-email2-javax/src/test/java/org/apache/commons/mail2/javax/EmailTest.java +++ b/commons-email2-javax/src/test/java/org/apache/commons/mail2/javax/EmailTest.java @@ -19,6 +19,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -571,6 +572,17 @@ public void testGetSetAuthentication() { assertEquals(strPassword, retrievedAuth.getPasswordAuthentication().getPassword()); } + @Test + public void testSetOAuth2Required() throws EmailException { + email.setHostName(strTestMailServer); + email.setOAuth2Required(true); + final Session mailSession = email.getMailSession(); + + // tests + assertNotNull(mailSession); + assertEquals("XOAUTH2", mailSession.getProperties().getProperty("mail.smtp.auth.mechanisms")); + } + @Test public void testGetSetAuthenticator() { // setup diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 6e98fd333..57cbd3f43 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -24,6 +24,7 @@ + Support for OAuth2 authentication. Handle IllegalArgumentException thrown for invalid email address #328. @@ -57,7 +58,7 @@ Fix broken JDK 9 build by updating "easymock" and other dependencies. - Use JUnit 5 APIs #106, #108, #109, #114. + Use JUnit 5 APIs #106, #108, #109, #114. [StepSecurity] ci: Harden GitHub Actions #149. Better use of JUnit APIs #158. Update conversion code #153. @@ -307,29 +308,29 @@ downloaded from HTTP or from the local file system. - Calling buildMimeMessage() before invoking send() caused - duplicated mime parts for HtmlEmail. The implementation now enforces - that an email can be only used once and throw an exception when - multiple invocations of buildMimeMessage() are detected. + Calling buildMimeMessage() before invoking send() caused + duplicated mime parts for HtmlEmail. The implementation now enforces + that an email can be only used once and throw an exception when + multiple invocations of buildMimeMessage() are detected. Incorrect SMTP Port number shown in error message when an email fails to send due to a blocked port and SSL is used. - + - Changing groupId from "commons-email" to "org.apache.commons" + Changing groupId from "commons-email" to "org.apache.commons" because the 1.1 release was already using "org.apache.commons" - + Using "http://example.invalid" for a bad url - ".invalid" is reserved - and not intended to be installed as a top-level domain in the global + and not intended to be installed as a top-level domain in the global Domain Name System (DNS) of the Internet. - + Made BaseEmailTestCase abstract and fixed a few coding issues. - + HtmlEmail invokes java.net.URL.equals(Object), which blocks to do domain name resolution. This is avoided by using "url.toExternalForm().equals()" instead of "url.equals()". @@ -435,7 +436,7 @@ - Make sure that the unit tests don't fail under JDK 1.3.x with + Make sure that the unit tests don't fail under JDK 1.3.x with java.net.BindException: Address already in use diff --git a/src/site/xdoc/userguide.xml b/src/site/xdoc/userguide.xml index 0edce5a01..3f60e9bab 100644 --- a/src/site/xdoc/userguide.xml +++ b/src/site/xdoc/userguide.xml @@ -337,6 +337,23 @@ import org.apache.commons.mail2.javax.*; to a known good address.

+
+

+ Checklist provided by user mkomko to help future users of this feature +

    +
  • Create an app registration in Entra ID with the permission "Office 365 Exchange Online - SMTP.SendAsApp"
  • +
  • Create a service principal in Exchange Online for the app and give it permissions to the mailbox you want to send as, as described in https://learn.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth#use-client-credentials-grant-flow-to-authenticate-smtp-imap-and-pop-connections
  • +
  • Get an Exchange Online access token (for example using the Graph API with the scope https://outlook.office365.com/.default)
  • +
  • SMTP server: smtp.office365.com, Port 587, StartTLS
  • +
  • Username: The email address of the mailbox you want to send as
  • +
  • Password: The access token
  • +
+

+ +