Email.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.commons.mail2.jakarta;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import org.apache.commons.mail2.core.EmailConstants;
import org.apache.commons.mail2.core.EmailException;
import org.apache.commons.mail2.core.EmailUtils;
import org.apache.commons.mail2.jakarta.util.IDNEmailAddressConverter;
import jakarta.mail.Authenticator;
import jakarta.mail.Message;
import jakarta.mail.MessagingException;
import jakarta.mail.Session;
import jakarta.mail.Store;
import jakarta.mail.Transport;
import jakarta.mail.internet.AddressException;
import jakarta.mail.internet.InternetAddress;
import jakarta.mail.internet.MimeMessage;
import jakarta.mail.internet.MimeMultipart;
import jakarta.mail.internet.MimeUtility;
/**
* The abstract class for all email messages. This class sets the sender's email, name, receiver's email, name, subject, and send date.
* <p>
* Subclasses are responsible for setting the message body.
* </p>
*
* @since 1.0
*/
public abstract class Email {
/**
* Empty array.
*/
private static final InternetAddress[] EMPTY_INTERNET_ADDRESS_ARRAY = {};
/**
* The email message to send.
*/
private MimeMessage message;
/**
* The charset to use for this message.
*/
private String charset;
/**
* The Address of the sending party, mandatory.
*/
private InternetAddress fromAddress;
/**
* The Subject.
*/
private String subject;
/**
* An attachment.
*/
private MimeMultipart emailBody;
/**
* The content.
*/
private Object content;
/**
* The content type.
*/
private String contentType;
/**
* Set session debugging on or off.
*/
private boolean debug;
/**
* Sent date.
*/
private Date sentDate;
/**
* Instance of an {@code Authenticator} object that will be used when authentication is requested from the mail server.
*/
private Authenticator authenticator;
/**
* The hostname of the mail server with which to connect. If null will try to get property from system.properties. If still null, quit.
*/
private String hostName;
/**
* The port number of the mail server to connect to. Defaults to the standard port ( 25 ).
*/
private String smtpPort = "25";
/**
* The port number of the SSL enabled SMTP server; defaults to the standard port, 465.
*/
private String sslSmtpPort = "465";
/**
* List of "to" email addresses.
*/
private List<InternetAddress> toList = new ArrayList<>();
/**
* List of "cc" email addresses.
*/
private List<InternetAddress> ccList = new ArrayList<>();
/**
* List of "bcc" email addresses.
*/
private List<InternetAddress> bccList = new ArrayList<>();
/**
* List of "replyTo" email addresses.
*/
private List<InternetAddress> replyList = new ArrayList<>();
/**
* Address to which undeliverable mail should be sent. Because this is handled by JavaMail as a String property in the mail session, this property is of
* type {@code String} rather than {@code InternetAddress}.
*/
private String bounceAddress;
/**
* Used to specify the mail headers. Example:
*
* X-Mailer: Sendmail, X-Priority: 1( highest ) or 2( high ) 3( normal ) 4( low ) and 5( lowest ) Disposition-Notification-To: user@domain.net
*/
private final Map<String, String> headers = new HashMap<>();
/**
* Whether to use POP3 before SMTP, and if so the settings.
*/
private boolean popBeforeSmtp;
/**
* The host name of the POP3 server.
*/
private String popHost;
/**
* The user name to log into the POP3 server.
*/
private String popUsername;
/**
* The password to log into the POP3 server.
*/
private String popPassword;
/**
* Does server require TLS encryption for authentication?
*/
private boolean tls;
/**
* Does the current transport use SSL/TLS encryption upon connection?
*/
private boolean ssl;
/**
* Socket I/O timeout value in milliseconds.
*/
private int socketTimeout = Math.toIntExact(EmailConstants.SOCKET_TIMEOUT.toMillis());
/**
* Socket connection timeout value in milliseconds.
*/
private int socketConnectionTimeout = Math.toIntExact(EmailConstants.SOCKET_TIMEOUT.toMillis());
/**
* If true, enables the use of the STARTTLS command (if supported by the server) to switch the connection to a TLS-protected connection before issuing any
* login commands. Note that an appropriate trust store must configured so that the client will trust the server's certificate. Defaults to false.
*/
private boolean startTlsEnabled;
/**
* If true, requires the use of the STARTTLS command. If the server doesn't support the STARTTLS command, or the command fails, the connect method will
* fail. Defaults to false.
*/
private boolean startTlsRequired;
/**
* Does the current transport use SSL/TLS encryption upon connection?
*/
private boolean sslOnConnect;
/**
* If set to true, check the server identity as specified by RFC 2595. These additional checks based on the content of the server's certificate are intended
* to prevent man-in-the-middle attacks. Defaults to false.
*/
private boolean sslCheckServerIdentity;
/**
* If set to true, and a message has some valid and some invalid addresses, send the message anyway, reporting the partial failure with a
* SendFailedException. If set to false (the default), the message is not sent to any of the recipients if there is an invalid recipient address. Defaults
* to false.
*/
private boolean sendPartial;
/**
* The Session to mail with.
*/
private Session session;
/**
* Constructs a new instance.
*/
public Email() {
// empty
}
/**
* Adds a blind BCC recipient to the email. The email address will also be used as the personal name. The name will be encoded by the charset of
* {@link #setCharset(String)}. If it is not set, it will be encoded using the Java platform's default charset (UTF-16) if it contains non-ASCII characters;
* otherwise, it is used as is.
*
* @param email A String.
* @return An Email.
* @throws EmailException Indicates an invalid email address
* @since 1.0
*/
public Email addBcc(final String email) throws EmailException {
return addBcc(email, null);
}
/**
* Adds an array of blind BCC recipients to the email. The email addresses will also be used as the personal name. The names will be encoded by the charset
* of {@link #setCharset(String)}. If it is not set, it will be encoded using the Java platform's default charset (UTF-16) if it contains non-ASCII
* characters; otherwise, it is used as is.
*
* @param emails A String array.
* @return An Email.
* @throws EmailException Indicates an invalid email address
* @since 1.3
*/
public Email addBcc(final String... emails) throws EmailException {
EmailException.checkNonEmpty(emails, () -> "BCC list invalid.");
for (final String email : emails) {
addBcc(email, null);
}
return this;
}
/**
* Adds a blind BCC recipient to the email using the specified address and the specified personal name. The name will be encoded by the charset of
* {@link #setCharset(String)}. If it is not set, it will be encoded using the Java platform's default charset (UTF-16) if it contains non-ASCII characters;
* otherwise, it is used as is.
*
* @param email A String.
* @param name A String.
* @return An Email.
* @throws EmailException Indicates an invalid email address
* @since 1.0
*/
public Email addBcc(final String email, final String name) throws EmailException {
return addBcc(email, name, charset);
}
/**
* Adds a blind BCC recipient to the email using the specified address, personal name, and charset encoding for the name.
*
* @param email A String.
* @param name A String.
* @param charset The charset to encode the name with.
* @return An Email.
* @throws EmailException Indicates an invalid email address
* @since 1.1
*/
public Email addBcc(final String email, final String name, final String charset) throws EmailException {
bccList.add(createInternetAddress(email, name, charset));
return this;
}
/**
* Adds a recipient CC to the email. The email address will also be used as the personal name. The name will be encoded by the charset of
* {@link #setCharset(String)}. If it is not set, it will be encoded using the Java platform's default charset (UTF-16) if it contains non-ASCII characters;
* otherwise, it is used as is.
*
* @param email A String.
* @return An Email.
* @throws EmailException Indicates an invalid email address.
* @since 1.0
*/
public Email addCc(final String email) throws EmailException {
return addCc(email, null);
}
/**
* Adds an array of CC recipients to the email. The email addresses will also be used as the personal name. The names will be encoded by the charset of
* {@link #setCharset(String)}. If it is not set, it will be encoded using the Java platform's default charset (UTF-16) if it contains non-ASCII characters;
* otherwise, it is used as is.
*
* @param emails A String array.
* @return An Email.
* @throws EmailException Indicates an invalid email address.
* @since 1.3
*/
public Email addCc(final String... emails) throws EmailException {
EmailException.checkNonEmpty(emails, () -> "CC list invalid.");
for (final String email : emails) {
addCc(email, null);
}
return this;
}
/**
* Adds a recipient CC to the email using the specified address and the specified personal name. The name will be encoded by the charset of
* {@link #setCharset(String)}. If it is not set, it will be encoded using the Java platform's default charset (UTF-16) if it contains non-ASCII characters;
* otherwise, it is used as is.
*
* @param email A String.
* @param name A String.
* @return An Email.
* @throws EmailException Indicates an invalid email address.
* @since 1.0
*/
public Email addCc(final String email, final String name) throws EmailException {
return addCc(email, name, charset);
}
/**
* Adds a recipient CC to the email using the specified address, personal name, and charset encoding for the name.
*
* @param email A String.
* @param name A String.
* @param charset The charset to encode the name with.
* @return An Email.
* @throws EmailException Indicates an invalid email address or charset.
* @since 1.1
*/
public Email addCc(final String email, final String name, final String charset) throws EmailException {
ccList.add(createInternetAddress(email, name, charset));
return this;
}
/**
* Adds a header ( name, value ) to the headers Map.
*
* @param name A String with the name.
* @param value A String with the value.
* @since 1.0
* @throws IllegalArgumentException if either {@code name} or {@code value} is null or empty
*/
public void addHeader(final String name, final String value) {
if (EmailUtils.isEmpty(name)) {
throw new IllegalArgumentException("name can not be null or empty");
}
if (EmailUtils.isEmpty(value)) {
throw new IllegalArgumentException("value can not be null or empty");
}
headers.put(name, value);
}
/**
* Adds a reply to address to the email. The email address will also be used as the personal name. The name will be encoded by the charset of
* {@link #setCharset(String)}. If it is not set, it will be encoded using the Java platform's default charset (UTF-16) if it contains non-ASCII characters;
* otherwise, it is used as is.
*
* @param email A String.
* @return An Email.
* @throws EmailException Indicates an invalid email address
* @since 1.0
*/
public Email addReplyTo(final String email) throws EmailException {
return addReplyTo(email, null);
}
/**
* Adds a reply to address to the email using the specified address and the specified personal name. The name will be encoded by the charset of
* {@link #setCharset(String)}. If it is not set, it will be encoded using the Java platform's default charset (UTF-16) if it contains non-ASCII characters;
* otherwise, it is used as is.
*
* @param email A String.
* @param name A String.
* @return An Email.
* @throws EmailException Indicates an invalid email address
* @since 1.0
*/
public Email addReplyTo(final String email, final String name) throws EmailException {
return addReplyTo(email, name, charset);
}
/**
* Adds a reply to address to the email using the specified address, personal name, and charset encoding for the name.
*
* @param email A String.
* @param name A String.
* @param charset The charset to encode the name with.
* @return An Email.
* @throws EmailException Indicates an invalid email address or charset.
* @since 1.1
*/
public Email addReplyTo(final String email, final String name, final String charset) throws EmailException {
replyList.add(createInternetAddress(email, name, charset));
return this;
}
/**
* Adds a recipient TO to the email. The email address will also be used as the personal name. The name will be encoded by the charset of
* {@link #setCharset(String)}. If it is not set, it will be encoded using the Java platform's default charset (UTF-16) if it contains non-ASCII characters;
* otherwise, it is used as is.
*
* @param email A String.
* @return An Email.
* @throws EmailException Indicates an invalid email address.
* @since 1.0
*/
public Email addTo(final String email) throws EmailException {
return addTo(email, null);
}
/**
* Adds a list of TO recipients to the email. The email addresses will also be used as the personal names. The names will be encoded by the charset of
* {@link #setCharset(String)}. If it is not set, it will be encoded using the Java platform's default charset (UTF-16) if it contains non-ASCII characters;
* otherwise, it is used as is.
*
* @param emails A String array.
* @return An Email.
* @throws EmailException Indicates an invalid email address.
* @since 1.3
*/
public Email addTo(final String... emails) throws EmailException {
EmailException.checkNonEmpty(emails, () -> "To list invalid.");
for (final String email : emails) {
addTo(email, null);
}
return this;
}
/**
* Adds a recipient TO to the email using the specified address and the specified personal name. The name will be encoded by the charset of
* {@link #setCharset(String)}. If it is not set, it will be encoded using the Java platform's default charset (UTF-16) if it contains non-ASCII characters;
* otherwise, it is used as is.
*
* @param email A String.
* @param name A String.
* @return An Email.
* @throws EmailException Indicates an invalid email address.
* @since 1.0
*/
public Email addTo(final String email, final String name) throws EmailException {
return addTo(email, name, charset);
}
/**
* Adds a recipient TO to the email using the specified address, personal name, and charset encoding for the name.
*
* @param email A String.
* @param name A String.
* @param charset The charset to encode the name with.
* @return An Email.
* @throws EmailException Indicates an invalid email address or charset.
* @since 1.1
*/
public Email addTo(final String email, final String name, final String charset) throws EmailException {
toList.add(createInternetAddress(email, name, charset));
return this;
}
/**
* Builds the MimeMessage. Please note that a user rarely calls this method directly and only if he/she is interested in the sending the underlying
* MimeMessage without commons-email.
*
* @throws IllegalStateException if the MimeMessage was already built
* @throws EmailException if there was an error.
* @since 1.0
*/
public void buildMimeMessage() throws EmailException {
if (message != null) {
// [EMAIL-95] we assume that an email is not reused therefore invoking
// buildMimeMessage() more than once is illegal.
throw new IllegalStateException("The MimeMessage is already built.");
}
try {
message = createMimeMessage(getMailSession());
if (EmailUtils.isNotEmpty(subject)) {
if (EmailUtils.isNotEmpty(charset)) {
message.setSubject(subject, charset);
} else {
message.setSubject(subject);
}
}
// update content type (and encoding)
updateContentType(contentType);
if (content != null) {
if (EmailConstants.TEXT_PLAIN.equalsIgnoreCase(contentType) && content instanceof String) {
// EMAIL-104: call explicitly setText to use default mime charset
// (property "mail.mime.charset") in case none has been set
message.setText(content.toString(), charset);
} else {
message.setContent(content, contentType);
}
} else if (emailBody != null) {
if (contentType == null) {
message.setContent(emailBody);
} else {
message.setContent(emailBody, contentType);
}
} else {
message.setText("");
}
if (fromAddress != null) {
message.setFrom(fromAddress);
} else if (session.getProperty(EmailConstants.MAIL_SMTP_FROM) == null && session.getProperty(EmailConstants.MAIL_FROM) == null) {
throw new EmailException("From address required");
}
if (toList.size() + ccList.size() + bccList.size() == 0) {
throw new EmailException("At least one receiver address required");
}
if (!EmailUtils.isEmpty(toList)) {
message.setRecipients(Message.RecipientType.TO, toInternetAddressArray(toList));
}
if (!EmailUtils.isEmpty(ccList)) {
message.setRecipients(Message.RecipientType.CC, toInternetAddressArray(ccList));
}
if (!EmailUtils.isEmpty(bccList)) {
message.setRecipients(Message.RecipientType.BCC, toInternetAddressArray(bccList));
}
if (!EmailUtils.isEmpty(replyList)) {
message.setReplyTo(toInternetAddressArray(replyList));
}
if (!EmailUtils.isEmpty(headers)) {
for (final Map.Entry<String, String> entry : headers.entrySet()) {
final String foldedValue = createFoldedHeaderValue(entry.getKey(), entry.getValue());
message.addHeader(entry.getKey(), foldedValue);
}
}
if (message.getSentDate() == null) {
message.setSentDate(getSentDate());
}
if (popBeforeSmtp) {
// TODO Why is this not a Store leak? When to close?
final Store store = session.getStore("pop3");
store.connect(popHost, popUsername, popPassword);
}
} catch (final MessagingException e) {
throw new EmailException(e);
}
}
/**
* When a mail session is already initialized setting the session properties has no effect. In order to flag the problem throw an IllegalStateException.
*
* @throws IllegalStateException when the mail session is already initialized
*/
private void checkSessionAlreadyInitialized() {
if (session != null) {
throw new IllegalStateException("The mail session is already initialized");
}
}
/**
* Creates a folded header value containing 76 character chunks.
*
* @param name the name of the header
* @param value the value of the header
* @return the folded header value
* @throws IllegalArgumentException if either the name or value is null or empty
*/
private String createFoldedHeaderValue(final String name, final String value) {
if (EmailUtils.isEmpty(name)) {
throw new IllegalArgumentException("name can not be null or empty");
}
if (EmailUtils.isEmpty(value)) {
throw new IllegalArgumentException("value can not be null or empty");
}
try {
return MimeUtility.fold(name.length() + 2, MimeUtility.encodeText(value, charset, null));
} catch (final UnsupportedEncodingException e) {
return value;
}
}
/**
* Creates an InternetAddress.
*
* @param email An email address.
* @param name A name.
* @param charsetName The name of the charset to encode the name with.
* @return An internet address.
* @throws EmailException Thrown when the supplied address, name or charset were invalid.
*/
private InternetAddress createInternetAddress(final String email, final String name, final String charsetName) throws EmailException {
try {
final InternetAddress address = new InternetAddress(new IDNEmailAddressConverter().toASCII(email));
// check name input
if (EmailUtils.isNotEmpty(name)) {
// check charset input.
if (EmailUtils.isEmpty(charsetName)) {
address.setPersonal(name);
} else {
// canonicalize the charset name and make sure
// the current platform supports it.
final Charset set = Charset.forName(charsetName);
address.setPersonal(name, set.name());
}
}
// run sanity check on new InternetAddress object; if this fails
// it will throw AddressException.
address.validate();
return address;
} catch (final AddressException | UnsupportedEncodingException e) {
throw new EmailException(e);
}
}
/**
* Creates a customized MimeMessage which can be implemented by a derived class, e.g. to set the message id.
*
* @param aSession mail session to be used
* @return the newly created message
*/
protected MimeMessage createMimeMessage(final Session aSession) {
return new MimeMessage(aSession);
}
/**
* Gets the authenticator.
*
* @return the authenticator.
* @since 1.6.0
*/
public Authenticator getAuthenticator() {
return authenticator;
}
/**
* Gets the list of "Bcc" addresses.
*
* @return List addresses
*/
public List<InternetAddress> getBccAddresses() {
return bccList;
}
/**
* Gets the "bounce address" of this email.
*
* @return the bounce address as string
* @since 1.4
*/
public String getBounceAddress() {
return bounceAddress;
}
/**
* Gets the list of "CC" addresses.
*
* @return List addresses
*/
public List<InternetAddress> getCcAddresses() {
return ccList;
}
/**
* Gets the Charset.
*
* @return the Charset.
* @since 1.6.0
*/
public String getCharsetName() {
return charset;
}
/**
* Gets the content.
*
* @return the content.
* @since 1.6.0
*/
public Object getContent() {
return content;
}
/**
* Gets the content type.
*
* @return the content type.
* @since 1.6.0
*/
public String getContentType() {
return contentType;
}
/**
* Gets the email body.
*
* @return the email body.
* @since 1.6.0
*/
public MimeMultipart getEmailBody() {
return emailBody;
}
/**
* Gets the sender of the email.
*
* @return from address
*/
public InternetAddress getFromAddress() {
return fromAddress;
}
/**
* Gets the specified header.
*
* @param header A string with the header.
* @return The value of the header, or null if no such header.
* @since 1.5
*/
public String getHeader(final String header) {
return headers.get(header);
}
/**
* Gets all headers on an Email.
*
* @return a Map of all headers.
* @since 1.5
*/
public Map<String, String> getHeaders() {
return headers;
}
/**
* Gets the host name of the SMTP server,
*
* @return host name
*/
public String getHostName() {
if (session != null) {
return session.getProperty(EmailConstants.MAIL_HOST);
}
if (EmailUtils.isNotEmpty(hostName)) {
return hostName;
}
return null;
}
/**
* Gets the mail session used when sending this Email, creating the Session if necessary. When a mail session is already initialized setting the session
* related properties will cause an IllegalStateException.
*
* @return A Session.
* @throws EmailException if the host name was not set
* @since 1.0
*/
public Session getMailSession() throws EmailException {
if (session == null) {
final Properties properties = new Properties(System.getProperties());
properties.setProperty(EmailConstants.MAIL_TRANSPORT_PROTOCOL, EmailConstants.SMTP);
if (EmailUtils.isEmpty(hostName)) {
hostName = properties.getProperty(EmailConstants.MAIL_HOST);
}
EmailException.checkNonEmpty(hostName, () -> "Cannot find valid hostname for mail session");
properties.setProperty(EmailConstants.MAIL_PORT, smtpPort);
properties.setProperty(EmailConstants.MAIL_HOST, hostName);
properties.setProperty(EmailConstants.MAIL_DEBUG, String.valueOf(debug));
properties.setProperty(EmailConstants.MAIL_TRANSPORT_STARTTLS_ENABLE, Boolean.toString(isStartTLSEnabled()));
properties.setProperty(EmailConstants.MAIL_TRANSPORT_STARTTLS_REQUIRED, Boolean.toString(isStartTLSRequired()));
properties.setProperty(EmailConstants.MAIL_SMTP_SEND_PARTIAL, Boolean.toString(isSendPartial()));
properties.setProperty(EmailConstants.MAIL_SMTPS_SEND_PARTIAL, Boolean.toString(isSendPartial()));
if (authenticator != null) {
properties.setProperty(EmailConstants.MAIL_SMTP_AUTH, "true");
}
if (isSSLOnConnect()) {
properties.setProperty(EmailConstants.MAIL_PORT, sslSmtpPort);
properties.setProperty(EmailConstants.MAIL_SMTP_SOCKET_FACTORY_PORT, sslSmtpPort);
properties.setProperty(EmailConstants.MAIL_SMTP_SOCKET_FACTORY_CLASS, "javax.net.ssl.SSLSocketFactory");
properties.setProperty(EmailConstants.MAIL_SMTP_SOCKET_FACTORY_FALLBACK, "false");
}
if ((isSSLOnConnect() || isStartTLSEnabled()) && isSSLCheckServerIdentity()) {
properties.setProperty(EmailConstants.MAIL_SMTP_SSL_CHECKSERVERIDENTITY, "true");
}
if (bounceAddress != null) {
properties.setProperty(EmailConstants.MAIL_SMTP_FROM, bounceAddress);
}
if (socketTimeout > 0) {
properties.setProperty(EmailConstants.MAIL_SMTP_TIMEOUT, Integer.toString(socketTimeout));
}
if (socketConnectionTimeout > 0) {
properties.setProperty(EmailConstants.MAIL_SMTP_CONNECTIONTIMEOUT, Integer.toString(socketConnectionTimeout));
}
// changed this (back) to getInstance due to security exceptions
// caused when testing using Maven
session = Session.getInstance(properties, authenticator);
}
return session;
}
/**
* Gets the message.
*
* @return the message.
* @since 1.6.0
*/
public MimeMessage getMessage() {
return message;
}
/**
* Gets the internal MimeMessage. Please note that the MimeMessage is built by the buildMimeMessage() method.
*
* @return the MimeMessage
*/
public MimeMessage getMimeMessage() {
return message;
}
/**
* Gets the POP3 host.
*
* @return the POP3 host.
* @since 1.6.0
*/
public String getPopHost() {
return popHost;
}
/**
* Gets the POP3 password.
*
* @return the POP3 password.
* @since 1.6.0
*/
public String getPopPassword() {
return popPassword;
}
/**
* Gets the POP3 user name.
*
* @return the POP3 user name.
* @since 1.6.0
*/
public String getPopUserName() {
return popUsername;
}
/**
* Gets the list of "Reply-To" addresses.
*
* @return List addresses
*/
public List<InternetAddress> getReplyToAddresses() {
return replyList;
}
/**
* Gets the sent date for the email.
*
* @return date to be used as the sent date for the email
* @since 1.0
*/
public Date getSentDate() {
if (sentDate == null) {
return new Date();
}
return new Date(sentDate.getTime());
}
/**
* Gets the listening port of the SMTP server.
*
* @return SMTP port
*/
public String getSmtpPort() {
if (session != null) {
return session.getProperty(EmailConstants.MAIL_PORT);
}
if (EmailUtils.isNotEmpty(smtpPort)) {
return smtpPort;
}
return null;
}
/**
* Gets the socket connection timeout value in milliseconds.
*
* @return the timeout in milliseconds.
* @since 1.2
*/
public int getSocketConnectionTimeout() {
return socketConnectionTimeout;
}
/**
* Gets the socket I/O timeout value in milliseconds.
*
* @return the socket I/O timeout
* @since 1.2
*/
public int getSocketTimeout() {
return socketTimeout;
}
/**
* Gets the current SSL port used by the SMTP transport.
*
* @return the current SSL port used by the SMTP transport
*/
public String getSslSmtpPort() {
if (session != null) {
return session.getProperty(EmailConstants.MAIL_SMTP_SOCKET_FACTORY_PORT);
}
if (EmailUtils.isNotEmpty(sslSmtpPort)) {
return sslSmtpPort;
}
return null;
}
/**
* Gets the subject of the email.
*
* @return email subject
*/
public String getSubject() {
return subject;
}
/**
* Gets the list of "To" addresses.
*
* @return List addresses
*/
public List<InternetAddress> getToAddresses() {
return toList;
}
/**
* Tests whether debug is on.
*
* @return whether debug is on.
* @since 1.6.0
*/
public boolean isDebug() {
return debug;
}
/**
* Tests whether to use POP3 before SMTP, and if so the settings.
*
* @return whether to use POP3 before SMTP, and if so the settings.
* @since 1.6.0
*/
public boolean isPopBeforeSmtp() {
return popBeforeSmtp;
}
/**
* Tests whether partial sending of email is enabled.
*
* @return true if sending partial email is enabled.
* @since 1.3.2
*/
public boolean isSendPartial() {
return sendPartial;
}
/**
* Tests whether the server identity checked as specified by RFC 2595
*
* @return true if the server identity is checked.
* @since 1.3
*/
public boolean isSSLCheckServerIdentity() {
return sslCheckServerIdentity;
}
/**
* Tests whether SSL/TLS encryption for the transport is currently enabled (SMTPS/POPS).
*
* @return true if SSL enabled for the transport.
* @since 1.3
*/
public boolean isSSLOnConnect() {
return sslOnConnect || ssl;
}
/**
* Tests whether the client is configured to try to enable STARTTLS.
*
* @return true if using STARTTLS for authentication, false otherwise.
* @since 1.3
*/
public boolean isStartTLSEnabled() {
return startTlsEnabled || tls;
}
/**
* Tests whether the client is configured to require STARTTLS.
*
* @return true if using STARTTLS for authentication, false otherwise.
* @since 1.3
*/
public boolean isStartTLSRequired() {
return startTlsRequired;
}
/**
* Sends the email. Internally we build a MimeMessage which is afterwards sent to the SMTP server.
*
* @return the message id of the underlying MimeMessage
* @throws IllegalStateException if the MimeMessage was already built, that is, {@link #buildMimeMessage()} was already called
* @throws EmailException the sending failed
*/
public String send() throws EmailException {
buildMimeMessage();
return sendMimeMessage();
}
/**
* Sends the previously created MimeMessage to the SMTP server.
*
* @return the message id of the underlying MimeMessage
* @throws IllegalArgumentException if the MimeMessage has not been created
* @throws EmailException the sending failed
*/
public String sendMimeMessage() throws EmailException {
Objects.requireNonNull(message, "MimeMessage has not been created yet");
try {
Transport.send(message);
return message.getMessageID();
} catch (final Throwable t) {
throw new EmailException("Sending the email to the following server failed : " + this.getHostName() + ":" + getSmtpPort(), t);
}
}
/**
* Sets the userName and password if authentication is needed. If this method is not used, no authentication will be performed.
* <p>
* This method will create a new instance of {@code DefaultAuthenticator} using the supplied parameters.
* </p>
*
* @param userName User name for the SMTP server
* @param password password for the SMTP server
* @see DefaultAuthenticator
* @see #setAuthenticator
* @since 1.0
*/
public void setAuthentication(final String userName, final String password) {
this.setAuthenticator(new DefaultAuthenticator(userName, password));
}
/**
* Sets the {@code Authenticator} to be used when authentication is requested from the mail server.
* <p>
* This method should be used when your outgoing mail server requires authentication. Your mail server must also support RFC2554.
* </p>
*
* @param authenticator the {@code Authenticator} object.
* @see Authenticator
* @since 1.0
*/
public void setAuthenticator(final Authenticator authenticator) {
this.authenticator = authenticator;
}
/**
* Sets a list of "BCC" addresses. All elements in the specified {@code Collection} are expected to be of type {@code java.mail.internet.InternetAddress}.
*
* @param collection collection of {@code InternetAddress} objects
* @return An Email.
* @throws EmailException Indicates an invalid email address
* @see jakarta.mail.internet.InternetAddress
* @since 1.0
*/
public Email setBcc(final Collection<InternetAddress> collection) throws EmailException {
EmailException.checkNonEmpty(collection, () -> "BCC list invalid");
bccList = new ArrayList<>(collection);
return this;
}
/**
* Sets the "bounce address" - the address to which undeliverable messages will be returned. If this value is never set, then the message will be sent to
* the address specified with the System property "mail.smtp.from", or if that value is not set, then to the "from" address.
*
* @param email A String.
* @return An Email.
* @throws IllegalStateException if the mail session is already initialized
* @since 1.0
*/
public Email setBounceAddress(final String email) {
checkSessionAlreadyInitialized();
if (!EmailUtils.isEmpty(email)) {
try {
bounceAddress = createInternetAddress(email, null, charset).getAddress();
} catch (final EmailException e) {
// Can't throw 'EmailException' to keep backward-compatibility
throw new IllegalArgumentException("Failed to set the bounce address : " + email, e);
}
} else {
bounceAddress = email;
}
return this;
}
/**
* Sets a list of "CC" addresses. All elements in the specified {@code Collection} are expected to be of type {@code java.mail.internet.InternetAddress}.
*
* @param collection collection of {@code InternetAddress} objects.
* @return An Email.
* @throws EmailException Indicates an invalid email address.
* @see jakarta.mail.internet.InternetAddress
* @since 1.0
*/
public Email setCc(final Collection<InternetAddress> collection) throws EmailException {
EmailException.checkNonEmpty(collection, () -> "CC list invalid");
ccList = new ArrayList<>(collection);
return this;
}
/**
* Sets the charset of the message. Please note that you should set the charset before adding the message content.
*
* @param charset A String.
* @throws java.nio.charset.IllegalCharsetNameException if the charset name is invalid
* @throws java.nio.charset.UnsupportedCharsetException if no support for the named charset exists in the current JVM
* @since 1.0
*/
public void setCharset(final String charset) {
final Charset set = Charset.forName(charset);
this.charset = set.name();
}
/**
* Sets the emailBody to a MimeMultiPart
*
* @param mimeMultipart aMimeMultipart
* @since 1.0
*/
public void setContent(final MimeMultipart mimeMultipart) {
this.emailBody = mimeMultipart;
}
/**
* Sets the content.
*
* @param content the content.
* @return {@code this} instance.
* @since 1.6.0
*/
public Email setContent(final Object content) {
this.content = content;
return this;
}
/**
* Sets the content and contentType.
*
* @param content content.
* @param contentType content type.
* @since 1.0
*/
public void setContent(final Object content, final String contentType) {
this.content = content;
updateContentType(contentType);
}
/**
* Sets the content type.
*
* @param contentType the content type.
* @return {@code this} instance.
* @since 1.6.0
*/
public Email setContentType(final String contentType) {
this.contentType = contentType;
return this;
}
/**
* Sets the display of debug information.
*
* @param debug A boolean.
* @since 1.0
*/
public void setDebug(final boolean debug) {
this.debug = debug;
}
/**
* Sets the FROM field of the email to use the specified address. The email address will also be used as the personal name. The name will be encoded by the
* charset of {@link #setCharset(String)}. If it is not set, it will be encoded using the Java platform's default charset (UTF-16) if it contains non-ASCII
* characters; otherwise, it is used as is.
*
* @param email A String.
* @return An Email.
* @throws EmailException Indicates an invalid email address.
* @since 1.0
*/
public Email setFrom(final String email) throws EmailException {
return setFrom(email, null);
}
/**
* Sets the FROM field of the email to use the specified address and the specified personal name. The name will be encoded by the charset of
* {@link #setCharset(String)}. If it is not set, it will be encoded using the Java platform's default charset (UTF-16) if it contains non-ASCII characters;
* otherwise, it is used as is.
*
* @param email A String.
* @param name A String.
* @return An Email.
* @throws EmailException Indicates an invalid email address.
* @since 1.0
*/
public Email setFrom(final String email, final String name) throws EmailException {
return setFrom(email, name, charset);
}
/**
* Sets the FROM field of the email to use the specified address, personal name, and charset encoding for the name.
*
* @param email A String.
* @param name A String.
* @param charset The charset to encode the name with.
* @return An Email.
* @throws EmailException Indicates an invalid email address or charset.
* @since 1.1
*/
public Email setFrom(final String email, final String name, final String charset) throws EmailException {
fromAddress = createInternetAddress(email, name, charset);
return this;
}
/**
* Sets the From address.
*
* @param fromAddress the From address.
* @return {@code this} instance.
* @since 1.6.0
*/
public Email setFromAddress(final InternetAddress fromAddress) {
this.fromAddress = fromAddress;
return this;
}
/**
* Sets the mail headers. Example:
*
* X-Mailer: Sendmail, X-Priority: 1( highest ) or 2( high ) 3( normal ) 4( low ) and 5( lowest ) Disposition-Notification-To: user@domain.net
*
* @param map A Map.
* @throws IllegalArgumentException if either of the provided header / value is null or empty
* @since 1.0
*/
public void setHeaders(final Map<String, String> map) {
headers.clear();
for (final Map.Entry<String, String> entry : map.entrySet()) {
addHeader(entry.getKey(), entry.getValue());
}
}
/**
* Sets the hostname of the outgoing mail server.
*
* @param hostName aHostName
* @throws IllegalStateException if the mail session is already initialized
* @since 1.0
*/
public void setHostName(final String hostName) {
checkSessionAlreadyInitialized();
this.hostName = hostName;
}
/**
* Sets a mail Session object to use. Please note that passing a user name and password (in the case of mail authentication) will create a new mail session
* with a DefaultAuthenticator. This is a convenience but might come unexpected.
*
* If mail authentication is used but NO user name and password is supplied the implementation assumes that you have set a authenticator and will use the
* existing mail session (as expected).
*
* @param session mail session to be used
* @throws NullPointerException if {@code aSession} is {@code null}
* @since 1.0
*/
public void setMailSession(final Session session) {
Objects.requireNonNull(session, "no mail session supplied");
final Properties sessionProperties = session.getProperties();
final String auth = sessionProperties.getProperty(EmailConstants.MAIL_SMTP_AUTH);
if (Boolean.parseBoolean(auth)) {
final String userName = sessionProperties.getProperty(EmailConstants.MAIL_SMTP_USER);
final String password = sessionProperties.getProperty(EmailConstants.MAIL_SMTP_PASSWORD);
if (EmailUtils.isNotEmpty(userName) && EmailUtils.isNotEmpty(password)) {
// only create a new mail session with an authenticator if
// authentication is required and no user name is given
authenticator = new DefaultAuthenticator(userName, password);
this.session = Session.getInstance(sessionProperties, authenticator);
} else {
// assume that the given mail session contains a working authenticator
this.session = session;
}
} else {
this.session = session;
}
}
/**
* Sets a mail Session object from a JNDI directory.
*
* @param jndiName name of JNDI resource (jakarta.mail.Session type), resource if searched in java:comp/env if name does not start with "java:"
* @throws IllegalArgumentException if the JNDI name is null or empty
* @throws NamingException if the resource cannot be retrieved from JNDI directory
* @since 1.1
*/
public void setMailSessionFromJNDI(final String jndiName) throws NamingException {
if (EmailUtils.isEmpty(jndiName)) {
throw new IllegalArgumentException("JNDI name missing");
}
Context ctx = null;
if (jndiName.startsWith("java:")) {
ctx = new InitialContext();
} else {
ctx = (Context) new InitialContext().lookup("java:comp/env");
}
setMailSession((Session) ctx.lookup(jndiName));
}
/**
* Sets the MIME message.
*
* @param message the MIME message.
*/
public void setMessage(final MimeMessage message) {
this.message = message;
}
/**
* Sets the content of the mail. It should be overridden by the subclasses.
*
* @param msg A String.
* @return An Email.
* @throws EmailException generic exception.
* @since 1.0
*/
public abstract Email setMsg(String msg) throws EmailException;
/**
* Sets whether to use POP3 before SMTP, and if so the settings.
*
* @param popBeforeSmtp whether to use POP3 before SMTP, and if so the settings.
* @return {@code this} instance.
* @since 1.6.0
*/
public Email setPopBeforeSmtp(final boolean popBeforeSmtp) {
this.popBeforeSmtp = popBeforeSmtp;
return this;
}
/**
* Sets details regarding "POP3 before SMTP" authentication.
*
* @param popBeforeSmtp Whether or not to log into POP3 server before sending mail.
* @param popHost The POP3 host to use.
* @param popUserName The POP3 user name.
* @param popPassword The POP3 password.
* @since 1.0
*/
public void setPopBeforeSmtp(final boolean popBeforeSmtp, final String popHost, final String popUserName, final String popPassword) {
this.popBeforeSmtp = popBeforeSmtp;
this.popHost = popHost;
this.popUsername = popUserName;
this.popPassword = popPassword;
}
/**
* Sets the POP3 host.
*
* @param popHost The POP3 host.
* @return {@code this} instance.
* @since 1.6.0
*/
public Email setPopHost(final String popHost) {
this.popHost = popHost;
return this;
}
/**
* Sets the POP3 password.
*
* @param popPassword the POP3 password.
* @return {@code this} instance.
* @since 1.6.0
*/
public Email setPopPassword(final String popPassword) {
this.popPassword = popPassword;
return this;
}
/**
* Sets the POP3 user name.
*
* @param popUserName the POP3 user name.
* @return {@code this} instance.
* @since 1.6.0
*/
public Email setPopUsername(final String popUserName) {
this.popUsername = popUserName;
return this;
}
/**
* Sets a list of reply to addresses. All elements in the specified {@code Collection} are expected to be of type
* {@code java.mail.internet.InternetAddress}.
*
* @param collection collection of {@code InternetAddress} objects
* @return An Email.
* @throws EmailException Indicates an invalid email address
* @see jakarta.mail.internet.InternetAddress
* @since 1.1
*/
public Email setReplyTo(final Collection<InternetAddress> collection) throws EmailException {
EmailException.checkNonEmpty(collection, () -> "Reply to list invalid");
replyList = new ArrayList<>(collection);
return this;
}
/**
* Sets whether the email is partially send in case of invalid addresses.
* <p>
* In case the mail server rejects an address as invalid, the call to {@link #send()} may throw a {@link jakarta.mail.SendFailedException}, even if partial
* send mode is enabled (emails to valid addresses will be transmitted). In case the email server does not reject invalid addresses immediately, but return
* a bounce message, no exception will be thrown by the {@link #send()} method.
* </p>
*
* @param sendPartial whether to enable partial send mode
* @return An Email.
* @throws IllegalStateException if the mail session is already initialized
* @since 1.3.2
*/
public Email setSendPartial(final boolean sendPartial) {
checkSessionAlreadyInitialized();
this.sendPartial = sendPartial;
return this;
}
/**
* Sets the sent date for the email. The sent date will default to the current date if not explicitly set.
*
* @param date Date to use as the sent date on the email
* @since 1.0
*/
public void setSentDate(final Date date) {
if (date != null) {
// create a separate instance to keep findbugs happy
sentDate = new Date(date.getTime());
}
}
/**
* Sets the non-SSL port number of the outgoing mail server.
*
* @param portNumber aPortNumber
* @throws IllegalArgumentException if the port number is < 1
* @throws IllegalStateException if the mail session is already initialized
* @since 1.0
* @see #setSslSmtpPort(String)
*/
public void setSmtpPort(final int portNumber) {
checkSessionAlreadyInitialized();
if (portNumber < 1) {
throw new IllegalArgumentException("Cannot connect to a port number that is less than 1 ( " + portNumber + " )");
}
this.smtpPort = Integer.toString(portNumber);
}
/**
* Sets the socket connection timeout value in milliseconds. Default is a 60 second timeout.
*
* @param socketConnectionTimeout the connection timeout
* @throws IllegalStateException if the mail session is already initialized
* @since 1.6.0
*/
public void setSocketConnectionTimeout(final Duration socketConnectionTimeout) {
checkSessionAlreadyInitialized();
this.socketConnectionTimeout = Math.toIntExact(socketConnectionTimeout.toMillis());
}
/**
* Sets the socket I/O timeout value in milliseconds. Default is 60 second timeout.
*
* @param socketTimeout the socket I/O timeout
* @throws IllegalStateException if the mail session is already initialized
* @since 1.6.0
*/
public void setSocketTimeout(final Duration socketTimeout) {
checkSessionAlreadyInitialized();
this.socketTimeout = Math.toIntExact(socketTimeout.toMillis());
}
/**
* Sets whether the server identity is checked as specified by RFC 2595
*
* @param sslCheckServerIdentity whether to enable server identity check
* @return An Email.
* @throws IllegalStateException if the mail session is already initialized
* @since 1.3
*/
public Email setSSLCheckServerIdentity(final boolean sslCheckServerIdentity) {
checkSessionAlreadyInitialized();
this.sslCheckServerIdentity = sslCheckServerIdentity;
return this;
}
/**
* Sets whether SSL/TLS encryption should be enabled for the SMTP transport upon connection (SMTPS/POPS). Takes precedence over
* {@link #setStartTLSRequired(boolean)}
* <p>
* Defaults to {@link #sslSmtpPort}; can be overridden by using {@link #setSslSmtpPort(String)}
* </p>
*
* @param ssl whether to enable the SSL transport
* @return An Email.
* @throws IllegalStateException if the mail session is already initialized
* @since 1.3
*/
public Email setSSLOnConnect(final boolean ssl) {
checkSessionAlreadyInitialized();
this.sslOnConnect = ssl;
this.ssl = ssl;
return this;
}
/**
* Sets the SSL port to use for the SMTP transport. Defaults to the standard port, 465.
*
* @param sslSmtpPort the SSL port to use for the SMTP transport
* @throws IllegalStateException if the mail session is already initialized
* @see #setSmtpPort(int)
*/
public void setSslSmtpPort(final String sslSmtpPort) {
checkSessionAlreadyInitialized();
this.sslSmtpPort = sslSmtpPort;
}
/**
* Sets or disable the STARTTLS encryption.
*
* @param startTlsEnabled true if STARTTLS requested, false otherwise
* @return An Email.
* @throws IllegalStateException if the mail session is already initialized
* @since 1.3
*/
public Email setStartTLSEnabled(final boolean startTlsEnabled) {
checkSessionAlreadyInitialized();
this.startTlsEnabled = startTlsEnabled;
this.tls = startTlsEnabled;
return this;
}
/**
* Sets or disable the required STARTTLS encryption.
* <p>
* Defaults to {@link #smtpPort}; can be overridden by using {@link #setSmtpPort(int)}
* </p>
*
* @param startTlsRequired true if STARTTLS requested, false otherwise
* @return An Email.
* @throws IllegalStateException if the mail session is already initialized
* @since 1.3
*/
public Email setStartTLSRequired(final boolean startTlsRequired) {
checkSessionAlreadyInitialized();
this.startTlsRequired = startTlsRequired;
return this;
}
/**
* Sets the email subject. Replaces end-of-line characters with spaces.
*
* @param aSubject A String.
* @return An Email.
* @since 1.0
*/
public Email setSubject(final String aSubject) {
this.subject = EmailUtils.replaceEndOfLineCharactersWithSpaces(aSubject);
return this;
}
/**
* Sets a list of "TO" addresses. All elements in the specified {@code Collection} are expected to be of type {@code java.mail.internet.InternetAddress}.
*
* @param collection collection of {@code InternetAddress} objects.
* @return An Email.
* @throws EmailException Indicates an invalid email address.
* @see jakarta.mail.internet.InternetAddress
* @since 1.0
*/
public Email setTo(final Collection<InternetAddress> collection) throws EmailException {
EmailException.checkNonEmpty(collection, () -> "To list invalid");
this.toList = new ArrayList<>(collection);
return this;
}
/**
* Converts to copy List of known InternetAddress objects into an array.
*
* @param list A List.
* @return An InternetAddress[].
* @since 1.0
*/
protected InternetAddress[] toInternetAddressArray(final List<InternetAddress> list) {
return list.toArray(EMPTY_INTERNET_ADDRESS_ARRAY);
}
/**
* Updates the contentType.
*
* @param contentType aContentType
* @since 1.2
*/
public void updateContentType(final String contentType) {
if (EmailUtils.isEmpty(contentType)) {
this.contentType = null;
} else {
// set the content type
this.contentType = contentType;
// set the charset if the input was properly formed
final String strMarker = "; charset=";
int charsetPos = EmailUtils.toLower(contentType).indexOf(strMarker);
if (charsetPos != -1) {
// find the next space (after the marker)
charsetPos += strMarker.length();
final int intCharsetEnd = EmailUtils.toLower(contentType).indexOf(" ", charsetPos);
if (intCharsetEnd != -1) {
this.charset = contentType.substring(charsetPos, intCharsetEnd);
} else {
this.charset = contentType.substring(charsetPos);
}
} else if (this.contentType.startsWith("text/") && EmailUtils.isNotEmpty(this.charset)) {
// use the default charset, if one exists, for messages
// whose content-type is some form of text.
final StringBuilder contentTypeBuf = new StringBuilder(this.contentType);
contentTypeBuf.append(strMarker);
contentTypeBuf.append(this.charset);
this.contentType = contentTypeBuf.toString();
}
}
}
}