/*
* 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.jmeter.protocol.smtp.sampler.protocol;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import javax.activation.DataHandler;
import javax.activation.FileDataSource;
import javax.mail.BodyPart;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import javax.net.ssl.SSLContext;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.jmeter.config.Argument;
import org.apache.jmeter.services.FileServer;
import org.apache.jmeter.testelement.property.CollectionProperty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class performs all tasks necessary to send a message (build message,
* prepare connection, send message). Provides getter-/setter-methods for an
* SmtpSampler-object to configure transport and message settings. The
* send-mail-command itself is started by the SmtpSampler-object.
*/
public class SendMailCommand {
// local vars
private static final Logger logger = LoggerFactory.getLogger(SendMailCommand.class);
// Use the actual class so the name must be correct.
private static final String TRUST_ALL_SOCKET_FACTORY = TrustAllSSLSocketFactory.class.getName();
private boolean useSSL = false;
private boolean useStartTLS = false;
private boolean trustAllCerts = false;
private boolean enforceStartTLS = false;
private boolean sendEmlMessage = false;
private boolean enableDebug;
private String smtpServer;
private String smtpPort;
private String sender;
private List<InternetAddress> replyTo;
private String emlMessage;
private List<InternetAddress> receiverTo;
private List<InternetAddress> receiverCC;
private List<InternetAddress> receiverBCC;
private CollectionProperty headerFields;
private String subject = "";
private boolean useAuthentication = false;
private String username;
private String password;
private boolean useLocalTrustStore;
private String trustStoreToUse;
private List<File> attachments;
private String mailBody;
private String timeOut; // Socket read timeout value in milliseconds. This timeout is implemented by java.net.Socket.
private String connectionTimeOut; // Socket connection timeout value in milliseconds. This timeout is implemented by java.net.Socket.
// case we are measuring real time of spedition
private boolean synchronousMode;
private Session session;
private StringBuilder serverResponse = new StringBuilder(); // TODO this is not populated currently
/** send plain body, i.e. not multipart/mixed */
private boolean plainBody;
/**
* Standard-Constructor
*/
public SendMailCommand() {
headerFields = new CollectionProperty();
attachments = new ArrayList<>();
}
/**
* Prepares message prior to be sent via execute()-method, i.e. sets
* properties such as protocol, authentication, etc.
*
* @return Message-object to be sent to execute()-method
* @throws MessagingException
* when problems constructing or sending the mail occur
* @throws IOException
* when the mail content can not be read or truststore problems
* are detected
*/
public Message prepareMessage() throws MessagingException, IOException {
Properties props = new Properties();
String protocol = getProtocol();
// set properties using JAF
props.setProperty("mail." + protocol + ".host", smtpServer);
props.setProperty("mail." + protocol + ".port", getPort());
props.setProperty("mail." + protocol + ".auth", Boolean.toString(useAuthentication));
// set timeout
props.setProperty("mail." + protocol + ".timeout", getTimeout());
props.setProperty("mail." + protocol + ".connectiontimeout", getConnectionTimeout());
if (useStartTLS || useSSL) {
try {
String allProtocols = StringUtils.join(
SSLContext.getDefault().getSupportedSSLParameters().getProtocols(), " ");
logger.info("Use ssl/tls protocols for mail: " + allProtocols);
props.setProperty("mail." + protocol + ".ssl.protocols", allProtocols);
} catch (Exception e) {
logger.error("Problem setting ssl/tls protocols for mail", e);
}
}
if (enableDebug) {
props.setProperty("mail.debug","true");
}
if (useStartTLS) {
props.setProperty("mail.smtp.starttls.enable", "true");
if (enforceStartTLS){
// Requires JavaMail 1.4.2+
props.setProperty("mail.smtp.starttls.require", "true");
}
}
if (trustAllCerts) {
if (useSSL) {
props.setProperty("mail.smtps.ssl.socketFactory.class", TRUST_ALL_SOCKET_FACTORY);
props.setProperty("mail.smtps.ssl.socketFactory.fallback", "false");
} else if (useStartTLS) {
props.setProperty("mail.smtp.ssl.socketFactory.class", TRUST_ALL_SOCKET_FACTORY);
props.setProperty("mail.smtp.ssl.socketFactory.fallback", "false");
}
} else if (useLocalTrustStore){
File truststore = new File(trustStoreToUse);
logger.info("load local truststore - try to load truststore from: "+truststore.getAbsolutePath());
if(!truststore.exists()){
logger.info("load local truststore -Failed to load truststore from: "+truststore.getAbsolutePath());
truststore = new File(FileServer.getFileServer().getBaseDir(), trustStoreToUse);
logger.info("load local truststore -Attempting to read truststore from: "+truststore.getAbsolutePath());
if(!truststore.exists()){
logger.info("load local truststore -Failed to load truststore from: "+truststore.getAbsolutePath() + ". Local truststore not available, aborting execution.");
throw new IOException("Local truststore file not found. Also not available under : " + truststore.getAbsolutePath());
}
}
if (useSSL) {
// Requires JavaMail 1.4.2+
props.put("mail.smtps.ssl.socketFactory", new LocalTrustStoreSSLSocketFactory(truststore));
props.put("mail.smtps.ssl.socketFactory.fallback", "false");
} else if (useStartTLS) {
// Requires JavaMail 1.4.2+
props.put("mail.smtp.ssl.socketFactory", new LocalTrustStoreSSLSocketFactory(truststore));
props.put("mail.smtp.ssl.socketFactory.fallback", "false");
}
}
session = Session.getInstance(props, null);
Message message;
if (sendEmlMessage) {
message = new MimeMessage(session, new BufferedInputStream(new FileInputStream(emlMessage)));
} else {
message = new MimeMessage(session);
// handle body and attachments
Multipart multipart = new MimeMultipart();
final int attachmentCount = attachments.size();
if (plainBody &&
(attachmentCount == 0 || (mailBody.length() == 0 && attachmentCount == 1))) {
if (attachmentCount == 1) { // i.e. mailBody is empty
File first = attachments.get(0);
try (FileInputStream fis = new FileInputStream(first);
InputStream is = new BufferedInputStream(fis)){
message.setText(IOUtils.toString(is, Charset.defaultCharset()));
}
} else {
message.setText(mailBody);
}
} else {
BodyPart body = new MimeBodyPart();
body.setText(mailBody);
multipart.addBodyPart(body);
for (File f : attachments) {
BodyPart attach = new MimeBodyPart();
attach.setFileName(f.getName());
attach.setDataHandler(new DataHandler(new FileDataSource(f.getAbsolutePath())));
multipart.addBodyPart(attach);
}
message.setContent(multipart);
}
}
// set from field and subject
if (null != sender) {
message.setFrom(new InternetAddress(sender));
}
if (null != replyTo) {
InternetAddress[] to = new InternetAddress[replyTo.size()];
message.setReplyTo(replyTo.toArray(to));
}
if(null != subject) {
message.setSubject(subject);
}
if (receiverTo != null) {
InternetAddress[] to = new InternetAddress[receiverTo.size()];
receiverTo.toArray(to);
message.setRecipients(Message.RecipientType.TO, to);
}
if (receiverCC != null) {
InternetAddress[] cc = new InternetAddress[receiverCC.size()];
receiverCC.toArray(cc);
message.setRecipients(Message.RecipientType.CC, cc);
}
if (receiverBCC != null) {
InternetAddress[] bcc = new InternetAddress[receiverBCC.size()];
receiverBCC.toArray(bcc);
message.setRecipients(Message.RecipientType.BCC, bcc);
}
for (int i = 0; i < headerFields.size(); i++) {
Argument argument = (Argument) headerFields.get(i).getObjectValue();
message.setHeader(argument.getName(), argument.getValue());
}
message.saveChanges();
return message;
}
/**
* Sends message to mailserver, waiting for delivery if using synchronous
* mode.
*
* @param message
* Message previously prepared by prepareMessage()
* @throws MessagingException
* when problems sending the mail arise
* @throws InterruptedException
* when interrupted while waiting for delivery in synchronous
* mode
*/
public void execute(Message message) throws MessagingException, InterruptedException {
Transport tr = null;
try {
tr = session.getTransport(getProtocol());
SynchronousTransportListener listener = null;
if (synchronousMode) {
listener = new SynchronousTransportListener();
tr.addTransportListener(listener);
}
if (useAuthentication) {
tr.connect(smtpServer, username, password);
} else {
tr.connect();
}
tr.sendMessage(message, message.getAllRecipients());
if (listener != null /*synchronousMode==true*/) {
listener.attend(); // listener cannot be null here
}
} finally {
if(tr != null) {
try {
tr.close();
} catch (Exception e) {
// NOOP
}
}
logger.debug("transport closed");
}
logger.debug("message sent");
}
/**
* Processes prepareMessage() and execute()
*
* @throws InterruptedException
* when interrupted while waiting for delivery in synchronous
* modus
* @throws IOException
* when the mail content can not be read or truststore problems
* are detected
* @throws MessagingException
* when problems constructing or sending the mail occur
*/
public void execute() throws MessagingException, IOException, InterruptedException {
execute(prepareMessage());
}
/**
* Returns FQDN or IP of SMTP-server to be used to send message - standard
* getter
*
* @return FQDN or IP of SMTP-server
*/
public String getSmtpServer() {
return smtpServer;
}
/**
* Sets FQDN or IP of SMTP-server to be used to send message - to be called
* by SmtpSampler-object
*
* @param smtpServer
* FQDN or IP of SMTP-server
*/
public void setSmtpServer(String smtpServer) {
this.smtpServer = smtpServer;
}
/**
* Returns sender-address for current message - standard getter
*
* @return sender-address
*/
public String getSender() {
return sender;
}
/**
* Sets the sender-address for the current message - to be called by
* SmtpSampler-object
*
* @param sender
* Sender-address for current message
*/
public void setSender(String sender) {
this.sender = sender;
}
/**
* Returns subject for current message - standard getter
*
* @return Subject of current message
*/
public String getSubject() {
return subject;
}
/**
* Sets subject for current message - called by SmtpSampler-object
*
* @param subject
* Subject for message of current message - may be null
*/
public void setSubject(String subject) {
this.subject = subject;
}
/**
* Returns username to authenticate at the mailserver - standard getter
*
* @return Username for mailserver
*/
public String getUsername() {
return username;
}
/**
* Sets username to authenticate at the mailserver - to be called by
* SmtpSampler-object
*
* @param username
* Username for mailserver
*/
public void setUsername(String username) {
this.username = username;
}
/**
* Returns password to authenticate at the mailserver - standard getter
*
* @return Password for mailserver
*/
public String getPassword() {
return password;
}
/**
* Sets password to authenticate at the mailserver - to be called by
* SmtpSampler-object
*
* @param password
* Password for mailserver
*/
public void setPassword(String password) {
this.password = password;
}
/**
* Sets receivers of current message ("to") - to be called by
* SmtpSampler-object
*
* @param receiverTo
* List of receivers
*/
public void setReceiverTo(List<InternetAddress> receiverTo) {
this.receiverTo = receiverTo;
}
/**
* Returns receivers of current message as {@link InternetAddress} ("cc") - standard
* getter
*
* @return List of receivers
*/
public List<InternetAddress> getReceiverCC() {
return receiverCC;
}
/**
* Sets receivers of current message ("cc") - to be called by
* SmtpSampler-object
*
* @param receiverCC
* List of receivers
*/
public void setReceiverCC(List<InternetAddress> receiverCC) {
this.receiverCC = receiverCC;
}
/**
* Returns receivers of current message as {@link InternetAddress} ("bcc") - standard
* getter
*
* @return List of receivers
*/
public List<InternetAddress> getReceiverBCC() {
return receiverBCC;
}
/**
* Sets receivers of current message ("bcc") - to be called by
* SmtpSampler-object
*
* @param receiverBCC
* List of receivers
*/
public void setReceiverBCC(List<InternetAddress> receiverBCC) {
this.receiverBCC = receiverBCC;
}
/**
* Returns if authentication is used to access the mailserver - standard
* getter
*
* @return True if authentication is used to access mailserver
*/
public boolean isUseAuthentication() {
return useAuthentication;
}
/**
* Sets if authentication should be used to access the mailserver - to be
* called by SmtpSampler-object
*
* @param useAuthentication
* Should authentication be used to access mailserver?
*/
public void setUseAuthentication(boolean useAuthentication) {
this.useAuthentication = useAuthentication;
}
/**
* Returns if SSL is used to send message - standard getter
*
* @return True if SSL is used to transmit message
*/
public boolean getUseSSL() {
return useSSL;
}
/**
* Sets SSL to secure the delivery channel for the message - to be called by
* SmtpSampler-object
*
* @param useSSL
* Should StartTLS be used to secure SMTP-connection?
*/
public void setUseSSL(boolean useSSL) {
this.useSSL = useSSL;
}
/**
* Returns if StartTLS is used to transmit message - standard getter
*
* @return True if StartTLS is used to transmit message
*/
public boolean getUseStartTLS() {
return useStartTLS;
}
/**
* Sets StartTLS to secure the delivery channel for the message - to be
* called by SmtpSampler-object
*
* @param useStartTLS
* Should StartTLS be used to secure SMTP-connection?
*/
public void setUseStartTLS(boolean useStartTLS) {
this.useStartTLS = useStartTLS;
}
/**
* Returns port to be used for SMTP-connection (standard 25 or 465) -
* standard getter
*
* @return Port to be used for SMTP-connection
*/
public String getSmtpPort() {
return smtpPort;
}
/**
* Sets port to be used for SMTP-connection (standard 25 or 465) - to be
* called by SmtpSampler-object
*
* @param smtpPort
* Port to be used for SMTP-connection
*/
public void setSmtpPort(String smtpPort) {
this.smtpPort = smtpPort;
}
/**
* Returns if sampler should trust all certificates - standard getter
*
* @return True if all Certificates are trusted
*/
public boolean isTrustAllCerts() {
return trustAllCerts;
}
/**
* Determines if SMTP-sampler should trust all certificates, no matter what
* CA - to be called by SmtpSampler-object
*
* @param trustAllCerts
* Should all certificates be trusted?
*/
public void setTrustAllCerts(boolean trustAllCerts) {
this.trustAllCerts = trustAllCerts;
}
/**
* Instructs object to enforce StartTLS and not to fallback to plain
* SMTP-connection - to be called by SmtpSampler-object
*
* @param enforceStartTLS
* Should StartTLS be enforced?
*/
public void setEnforceStartTLS(boolean enforceStartTLS) {
this.enforceStartTLS = enforceStartTLS;
}
/**
* Returns if StartTLS is enforced to secure the connection, i.e. no
* fallback is used (plain SMTP) - standard getter
*
* @return True if StartTLS is enforced
*/
public boolean isEnforceStartTLS() {
return enforceStartTLS;
}
/**
* Returns headers for current message - standard getter
*
* @return CollectionProperty of headers for current message
*/
public CollectionProperty getHeaders() {
return headerFields;
}
/**
* Sets headers for current message
*
* @param headerFields
* CollectionProperty of headers for current message
*/
public void setHeaderFields(CollectionProperty headerFields) {
this.headerFields = headerFields;
}
/**
* Adds a header-part to current HashMap of headers - to be called by
* SmtpSampler-object
*
* @param headerName
* Key for current header
* @param headerValue
* Value for current header
*/
public void addHeader(String headerName, String headerValue) {
if (this.headerFields == null){
this.headerFields = new CollectionProperty();
}
Argument argument = new Argument(headerName, headerValue);
this.headerFields.addItem(argument);
}
/**
* Deletes all current headers in HashMap
*/
public void clearHeaders() {
if (this.headerFields == null){
this.headerFields = new CollectionProperty();
}else{
this.headerFields.clear();
}
}
/**
* Returns all attachment for current message - standard getter
*
* @return List of attachments for current message
*/
public List<File> getAttachments() {
return attachments;
}
/**
* Adds attachments to current message
*
* @param attachments
* List of files to be added as attachments to current message
*/
public void setAttachments(List<File> attachments) {
this.attachments = attachments;
}
/**
* Adds an attachment to current message - to be called by
* SmtpSampler-object
*
* @param attachment
* File-object to be added as attachment to current message
*/
public void addAttachment(File attachment) {
this.attachments.add(attachment);
}
/**
* Clear all attachments for current message
*/
public void clearAttachments() {
this.attachments.clear();
}
/**
* Returns if synchronous-mode is used for current message (i.e. time for
* delivery, ... is measured) - standard getter
*
* @return True if synchronous-mode is used
*/
public boolean isSynchronousMode() {
return synchronousMode;
}
/**
* Sets the use of synchronous-mode (i.e. time for delivery, ... is
* measured) - to be called by SmtpSampler-object
*
* @param synchronousMode
* Should synchronous-mode be used?
*/
public void setSynchronousMode(boolean synchronousMode) {
this.synchronousMode = synchronousMode;
}
/**
* Returns which protocol should be used to transport message (smtps for
* SSL-secured connections or smtp for plain SMTP / StartTLS)
*
* @return Protocol that is used to transport message
*/
private String getProtocol() {
return useSSL ? "smtps" : "smtp";
}
/**
* Returns port to be used for SMTP-connection - returns the
* default port for the protocol if no port has been supplied.
*
* @return Port to be used for SMTP-connection
*/
private String getPort() {
String port = smtpPort.trim();
if (port.length() > 0) { // OK, it has been supplied
return port;
}
if (useSSL){
return "465";
}
if (useStartTLS) {
return "587";
}
return "25";
}
/**
* @param timeOut the timeOut to set
*/
public void setTimeOut(String timeOut) {
this.timeOut = timeOut;
}
/**
* Returns timeout for the SMTP-connection - returns the
* default timeout if no value has been supplied.
*
* @return Timeout to be set for SMTP-connection
*/
public String getTimeout() {
String timeout = timeOut.trim();
if (timeout.length() > 0) { // OK, it has been supplied
return timeout;
}
return "0"; // Default is infinite timeout (value 0).
}
/**
* @param connectionTimeOut the connectionTimeOut to set
*/
public void setConnectionTimeOut(String connectionTimeOut) {
this.connectionTimeOut = connectionTimeOut;
}
/**
* Returns connection timeout for the SMTP-connection - returns the
* default connection timeout if no value has been supplied.
*
* @return Connection timeout to be set for SMTP-connection
*/
public String getConnectionTimeout() {
String connectionTimeout = connectionTimeOut.trim();
if (connectionTimeout.length() > 0) { // OK, it has been supplied
return connectionTimeout;
}
return "0"; // Default is infinite timeout (value 0).
}
/**
* Assigns the object to use a local truststore for SSL / StartTLS - to be
* called by SmtpSampler-object
*
* @param useLocalTrustStore
* Should a local truststore be used?
*/
public void setUseLocalTrustStore(boolean useLocalTrustStore) {
this.useLocalTrustStore = useLocalTrustStore;
}
/**
* Sets the path to the local truststore to be used for SSL / StartTLS - to
* be called by SmtpSampler-object
*
* @param trustStoreToUse
* Path to local truststore
*/
public void setTrustStoreToUse(String trustStoreToUse) {
this.trustStoreToUse = trustStoreToUse;
}
public void setUseEmlMessage(boolean sendEmlMessage) {
this.sendEmlMessage = sendEmlMessage;
}
/**
* Sets eml-message to be sent
*
* @param emlMessage
* path to eml-message
*/
public void setEmlMessage(String emlMessage) {
this.emlMessage = emlMessage;
}
/**
* Set the mail body.
*
* @param body the body of the mail
*/
public void setMailBody(String body){
mailBody = body;
}
/**
* Set whether to send a plain body (i.e. not multipart/mixed)
*
* @param plainBody <code>true</code> if sending a plain body (i.e. not multipart/mixed)
*/
public void setPlainBody(boolean plainBody){
this.plainBody = plainBody;
}
public String getServerResponse() {
return this.serverResponse.toString();
}
public void setEnableDebug(boolean selected) {
enableDebug = selected;
}
public void setReplyTo(List<InternetAddress> replyTo) {
this.replyTo = replyTo;
}
}