Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Copy link
Member

Choose a reason for hiding this comment

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

The Javadoc since tag is missing.


/** The SMTP user name. */
public static final String MAIL_SMTP_USER = "mail.smtp.user";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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?
*/
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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"));
Copy link
Member

Choose a reason for hiding this comment

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

Reuse EmailConstants.

}

@Test
public void testGetSetAuthenticator() {
// setup
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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?
*/
Expand Down Expand Up @@ -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");
Copy link
Member

Choose a reason for hiding this comment

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

Refactor "XOAUTH2" into a constant.

}

if (isSSLOnConnect()) {
properties.setProperty(EmailConstants.MAIL_PORT, sslSmtpPort);
properties.setProperty(EmailConstants.MAIL_SMTP_SOCKET_FACTORY_PORT, sslSmtpPort);
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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"));
Copy link
Member

Choose a reason for hiding this comment

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

Reuse EmailConstants.

}

@Test
public void testGetSetAuthenticator() {
// setup
Expand Down
25 changes: 13 additions & 12 deletions src/changes/changes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
<body>
<release version="2.0.0-M2" date="YYY-MM-DD" description="Feature release (Java 8 or above).">
<!-- ADD -->
<action type="add" due-to="Siegfried Goeschl" dev="sgoeschl" issue="EMAIL-163">Support for OAuth2 authentication.</action>
<!-- FIX -->
<action type="update" due-to="Derek Wickern, Gary Gregory" dev="ggregory">Handle IllegalArgumentException thrown for invalid email address #328.</action>
<!-- UPDATE -->
Expand Down Expand Up @@ -57,7 +58,7 @@
<release version="1.6.0" date="2023-12-17" description="Feature release (Java 8 or above).">
<!-- FIX -->
<action issue="EMAIL-190" type="fix" due-to="sgoeschl">Fix broken JDK 9 build by updating "easymock" and other dependencies.</action>
<action type="fix" dev="ggregory" due-to="John Patrick, Gary Gregory">Use JUnit 5 APIs #106, #108, #109, #114.</action>
<action type="fix" dev="ggregory" due-to="John Patrick, Gary Gregory">Use JUnit 5 APIs #106, #108, #109, #114.</action>
<action dev="ggregory" type="fix" due-to="step-security-bot, Gary Gregory">[StepSecurity] ci: Harden GitHub Actions #149.</action>
<action dev="ggregory" type="fix" due-to="Lee Jaeheon, sebbASF">Better use of JUnit APIs #158.</action>
<action issue="EMAIL-205" dev="ggregory" type="fix" due-to="Dimitrios Efthymiou, Alex Herbert">Update conversion code #153.</action>
Expand Down Expand Up @@ -307,29 +308,29 @@
downloaded from HTTP or from the local file system.
</action>
<action dev="sgoeschl" type="fix" issue="EMAIL-95" date="2010-05-13">
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.
</action>
<action dev="sgoeschl" type="fix" issue="EMAIL-91" date="2010-05-13" due-to="Kevin Lester">
Incorrect SMTP Port number shown in error message when an email fails
to send due to a blocked port and SSL is used.
</action>
</action>
</release>
<release version="1.2" date="2009-10-26">
<action dev="sgoeschl" type="update" date="2009-06-26">
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"
</action>
</action>
<action dev="sgoeschl" type="fix" date="2009-06-16" due-to="sebb">
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.
</action>
</action>
<action dev="sgoeschl" type="fix" date="2009-06-16" due-to="sebb">
Made BaseEmailTestCase abstract and fixed a few coding issues.
</action>
</action>
<action dev="sgoeschl" type="fix" issue="EMAIL-87" date="2009-06-16" due-to="sebb">
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()".
Expand Down Expand Up @@ -435,7 +436,7 @@
<release version="1.0" date="2005-09-27"/>
<release version="1.0-rc8" date="2005-09-07">
<action dev="henning" type="fix">
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
</action>
<action dev="henning" type="update" due-to="Niall Pemberton" issue="EMAIL-49">
Expand Down
17 changes: 17 additions & 0 deletions src/site/xdoc/userguide.xml
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,23 @@ import org.apache.commons.mail2.javax.*;
to a known good address.
</p>
</section>
<section name="Using OAuth2 Authentication With Office 365">
<p>
Checklist provided by user <em>mkomko</em> to help future users of this feature
<ul>
<li>Create an app registration in Entra ID with the permission "Office 365 Exchange Online - SMTP.SendAsApp"</li>
<li>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</li>
<li>Get an Exchange Online access token (for example using the Graph API with the scope https://outlook.office365.com/.default)</li>
<li>SMTP server: smtp.office365.com, Port 587, StartTLS</li>
<li>Username: The email address of the mailbox you want to send as</li>
<li>Password: The access token</li>
</ul>
</p>
<source><![CDATA[
email.setOAuth2Required(true);
email.setAuthentication(emailAddress, accessToken);
]]></source>
</section>
</body>
</document>

Expand Down
Loading