package org.ovirt.engine.core.notifier.transport.smtp;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Date;
import java.util.Iterator;
import java.util.Properties;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;
import javax.mail.Address;
import javax.mail.Authenticator;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.PasswordAuthentication;
import javax.mail.Session;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import org.apache.commons.lang.StringUtils;
import org.ovirt.engine.core.common.EventNotificationMethod;
import org.ovirt.engine.core.notifier.dao.DispatchResult;
import org.ovirt.engine.core.notifier.filter.AuditLogEvent;
import org.ovirt.engine.core.notifier.transport.Transport;
import org.ovirt.engine.core.notifier.utils.NotificationProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The class sends e-mails to event subscribers.
* In order to define a proper mail client, the following properties should be provided:
* <ul>
* <li><code>MAIL_SERVER</code> mail server name
* <li><code>MAIL_PORT</code> mail server port</li>
* </ul>
*
* The following properties are optional:
* <ul>
* <li><code>MAIL_USER</code> user name includes a domain (e.g. user@test.com)</li>
* <li><code>MAIL_PASSWORD</code> user's password</li>
* <li>if failed to obtain or uses "localhost" if <code>MAIL_MACHINE_NAME</code> not provided</li>
* <li><code>MAIL_FROM</code> specifies "from" address in sent message, or uses value of property <code>MAIL_USER</code> if not provided</li>
* <li>"from" address should include a domain, same as <code>MAIL_USER</code> property</li>
* <li><code>MAIL_REPLY_TO</code> specifies "replyTo" address in outgoing message</li>
* </ul>
*/
public class Smtp extends Transport {
private static final String MAIL_SERVER = "MAIL_SERVER";
private static final String MAIL_PORT = "MAIL_PORT";
private static final String MAIL_USER = "MAIL_USER";
private static final String MAIL_PASSWORD = "MAIL_PASSWORD";
private static final String MAIL_FROM = "MAIL_FROM";
private static final String MAIL_REPLY_TO = "MAIL_REPLY_TO";
private static final String HTML_MESSAGE_FORMAT = "HTML_MESSAGE_FORMAT";
private static final String MAIL_SMTP_ENCRYPTION = "MAIL_SMTP_ENCRYPTION";
private static final String MAIL_SMTP_ENCRYPTION_NONE = "none";
private static final String MAIL_SMTP_ENCRYPTION_SSL = "ssl";
private static final String MAIL_SMTP_ENCRYPTION_TLS = "tls";
private static final String MAIL_SEND_INTERVAL = "MAIL_SEND_INTERVAL";
private static final String MAIL_RETRIES = "MAIL_RETRIES";
private static final Logger log = LoggerFactory.getLogger(Smtp.class);
private int retries;
private int sendIntervals;
private int lastSendInterval = 0;
private final Queue<DispatchAttempt> sendQueue = new LinkedBlockingQueue<>();
private String hostName;
private boolean isBodyHtml = false;
private Session session = null;
private InternetAddress from = null;
private InternetAddress replyTo = null;
private boolean active = false;
public Smtp(NotificationProperties props) {
if (!StringUtils.isEmpty(props.getProperty(MAIL_SERVER, true))) {
active = true;
init(props);
}
}
private void init(NotificationProperties props) {
Properties mailSessionProps = new Properties();
try {
hostName = InetAddress.getLocalHost().getHostName();
} catch (UnknownHostException e) {
Smtp.log.error("Failed to resolve machine name, using localhost instead.", e);
hostName = "localhost";
}
retries = props.validateNonNegetive(MAIL_RETRIES);
sendIntervals = props.validateNonNegetive(MAIL_SEND_INTERVAL);
isBodyHtml = props.getBoolean(HTML_MESSAGE_FORMAT, false);
from = props.validateEmail(MAIL_FROM);
replyTo = props.validateEmail(MAIL_REPLY_TO);
if (log.isTraceEnabled()) {
mailSessionProps.put("mail.debug", "true");
}
mailSessionProps.put("mail.smtp.host", props.getProperty(MAIL_SERVER));
mailSessionProps.put("mail.smtp.port", props.validatePort(MAIL_PORT));
String encryption = props.getProperty(MAIL_SMTP_ENCRYPTION);
if (MAIL_SMTP_ENCRYPTION_NONE.equals(encryption)) {
// Do nothing
} else if (MAIL_SMTP_ENCRYPTION_SSL.equals(encryption)) {
mailSessionProps.put("mail.smtp.auth", "true");
mailSessionProps.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
mailSessionProps.put("mail.smtp.socketFactory.fallback", false);
mailSessionProps.put("mail.smtp.socketFactory.port", props.validatePort(MAIL_PORT));
} else if (MAIL_SMTP_ENCRYPTION_TLS.equals(encryption)) {
mailSessionProps.put("mail.smtp.auth", "true");
mailSessionProps.put("mail.smtp.starttls.enable", "true");
mailSessionProps.put("mail.smtp.starttls.required", "true");
}
else {
throw new IllegalArgumentException(
String.format(
"Illegal encryption method for %s",
MAIL_SMTP_ENCRYPTION));
}
String emailUser = props.getProperty(MAIL_USER, true);
String emailPassword = props.getProperty(MAIL_PASSWORD, true);
if (StringUtils.isEmpty(emailUser) && StringUtils.isNotEmpty(emailPassword)) {
throw new IllegalArgumentException(
String.format(
"'%s' must be set when password is set",
MAIL_USER));
}
if (StringUtils.isNotEmpty(emailPassword)) {
session = Session.getDefaultInstance(mailSessionProps,
new EmailAuthenticator(emailUser, emailPassword));
} else {
session = Session.getInstance(mailSessionProps);
}
}
@Override
public String getName() {
return "smtp";
}
@Override
public boolean isActive() {
return active;
}
@Override
public void dispatchEvent(AuditLogEvent event, String address) {
if (StringUtils.isEmpty(address)) {
log.error("Address is empty, cannot distribute message. {}", event.getName());
}
else {
sendQueue.add(new DispatchAttempt(event, address));
}
}
@Override
public void idle() {
if (lastSendInterval++ >= sendIntervals) {
lastSendInterval = 0;
Iterator<DispatchAttempt> iterator = sendQueue.iterator();
while (iterator.hasNext()) {
DispatchAttempt attempt = iterator.next();
try {
EventMessageContent message = new EventMessageContent();
message.prepareMessage(hostName, attempt.event, isBodyHtml);
log.info("Sending e-mail subject='{}' to='{}'",
message.getMessageSubject(),
attempt.address);
log.debug("Send e-mail body='{}'", message.getMessageBody());
sendMail(attempt.address, message.getMessageSubject(), message.getMessageBody());
log.info(
"E-mail subject='{}' to='{}' sent successfully",
message.getMessageSubject(),
attempt.address
);
notifyObservers(DispatchResult.success(attempt.event, attempt.address, EventNotificationMethod.SMTP));
iterator.remove();
} catch (Exception ex) {
attempt.retries++;
if (attempt.retries >= retries) {
notifyObservers(DispatchResult.failure(attempt.event,
attempt.address,
EventNotificationMethod.SMTP,
ex.getMessage()));
iterator.remove();
}
}
}
}
}
/**
* Sends a message to a recipient using pre-configured mail session, either as a plan text message or as a html
* message body
* @param recipient
* a recipient mail address
* @param messageSubject
* the subject of the message
* @param messageBody
* the body of the message
*/
private void sendMail(String recipient, String messageSubject, String messageBody) throws MessagingException {
try {
Message msg = new MimeMessage(session);
msg.setFrom(from);
InternetAddress address = new InternetAddress(recipient);
msg.setRecipient(Message.RecipientType.TO, address);
if (replyTo != null) {
msg.setReplyTo(new Address[] { replyTo });
}
msg.setSubject(messageSubject);
if (isBodyHtml){
msg.setContent(String.format("<html><head><title>%s</title></head><body><p>%s</body></html>",
messageSubject,
messageBody), "text/html");
} else {
msg.setText(messageBody);
}
msg.setSentDate(new Date());
javax.mail.Transport.send(msg);
} catch (MessagingException mex) {
StringBuilder errorMsg = new StringBuilder("Failed to send message ");
if (from != null) {
errorMsg.append(" from ").append(from.toString());
}
if (StringUtils.isNotBlank(recipient)) {
errorMsg.append(" to ").append(recipient);
}
if (StringUtils.isNotBlank(messageSubject)) {
errorMsg.append(" with subject ").append(messageSubject);
}
errorMsg.append(" due to to error: ").append(mex.getMessage());
log.error(errorMsg.toString(), mex);
throw mex;
}
}
/**
* An implementation of the {@link Authenticator}, holds the authentication credentials for a network connection.
*/
private static class EmailAuthenticator extends Authenticator {
private String userName;
private String password;
public EmailAuthenticator(String userName, String password) {
this.userName = userName;
this.password = password;
}
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(userName, password);
}
}
private static class DispatchAttempt {
public final AuditLogEvent event;
public final String address;
public int retries = 0;
private DispatchAttempt(AuditLogEvent event, String address) {
this.event = event;
this.address = address;
}
}
}