/*******************************************************************************
* This file is part of OpenNMS(R).
*
* Copyright (C) 2008-2011 The OpenNMS Group, Inc.
* OpenNMS(R) is Copyright (C) 1999-2011 The OpenNMS Group, Inc.
*
* OpenNMS(R) is a registered trademark of The OpenNMS Group, Inc.
*
* OpenNMS(R) is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published
* by the Free Software Foundation, either version 3 of the License,
* or (at your option) any later version.
*
* OpenNMS(R) is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with OpenNMS(R). If not, see:
* http://www.gnu.org/licenses/
*
* For more information contact:
* OpenNMS(R) Licensing <license@opennms.org>
* http://www.opennms.org/
* http://www.opennms.com/
*******************************************************************************/
package org.opennms.netmgt.poller.monitors;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import javax.mail.Flags.Flag;
import javax.mail.Folder;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.Store;
import javax.mail.search.HeaderTerm;
import javax.mail.search.SearchTerm;
import javax.mail.search.SubjectTerm;
import org.opennms.core.utils.LogUtils;
import org.opennms.core.utils.TimeoutTracker;
import org.opennms.javamail.JavaMailer;
import org.opennms.javamail.JavaMailerException;
import org.opennms.netmgt.config.mailtransporttest.JavamailProperty;
import org.opennms.netmgt.config.mailtransporttest.ReadmailTest;
import org.opennms.netmgt.config.mailtransporttest.SendmailTest;
import org.opennms.netmgt.model.PollStatus;
import org.opennms.netmgt.poller.Distributable;
import org.opennms.netmgt.poller.MonitoredService;
//TODO: adjust to use new javamail-configuration.xml
/**
* This <code>ServiceMonitor</code> is designed to monitor the transport of
* SMTP email.
*
* Use cases:
*
* a) Class will test that it can successfully send an email.
* b) Class will test that it can successfully connect to a mail server and get mailbox contents.
* c) Class will test that it can successfully read a new email message from a mail server.
* d) Class will test that it can send an email and read that same email from a mail server.
*
* @author <a href="mailto:david@opennms.org">David Hustace</a>
* @version $Id: $
*/
@Distributable
public class MailTransportMonitor extends AbstractServiceMonitor {
private static final String MTM_HEADER_KEY = "X-OpenNMS-MTM-ID";
private final String m_headerValue = Integer.toString(new Random().nextInt(Integer.MAX_VALUE));
/** {@inheritDoc} */
@Override
public PollStatus poll(MonitoredService svc, Map<String, Object> parameters) {
PollStatus status = null;
try {
final MailTransportParameters mailParms = MailTransportParameters.get(parameters);
try {
if ("${ipaddr}".equals(mailParms.getReadTestHost())) {
mailParms.setReadTestHost(svc.getIpAddr());
}
} catch (final IllegalStateException ise) {
//just ignore, don't have to have a both a read and send test configured
}
try {
if ("${ipaddr}".equals(mailParms.getSendTestHost())) {
mailParms.setSendTestHost(svc.getIpAddr());
}
} catch (final IllegalStateException ise) {
//just ignore, don't have to have a both a read and send test configured
}
parseJavaMailProperties(mailParms);
status = doMailTest(mailParms);
} catch (final IllegalStateException ise) {
//ignore this because we don't have to have both a send and read
} catch (final Throwable e) {
LogUtils.errorf(this, e, "An error occurred while polling.");
status = PollStatus.down("Exception from mailer: " + e.getLocalizedMessage());
}
return status;
}
private void parseJavaMailProperties(final MailTransportParameters mailParms) {
final ReadmailTest readTest = mailParms.getReadTest();
List<JavamailProperty> propertyList = new ArrayList<JavamailProperty>();
if (readTest != null) {
propertyList = readTest.getJavamailPropertyCollection();
}
final SendmailTest sendTest = mailParms.getSendTest();
if (sendTest != null) {
final List<JavamailProperty> sendTestProperties = sendTest.getJavamailPropertyCollection();
propertyList.addAll(sendTestProperties);
}
final Properties props = mailParms.getJavamailProperties();
for (final JavamailProperty property : propertyList) {
props.setProperty(property.getName(), property.getValue());
}
mailParms.setJavamailProperties(props);
}
/**
* This method handles all the logic for testing mail.
*
* @param mailParms
*/
private PollStatus doMailTest(final MailTransportParameters mailParms) {
final long beginPoll = System.currentTimeMillis();
PollStatus status = PollStatus.unknown("Beginning poll.");
mailParms.setTestSubjectSuffix(Long.toString(beginPoll));
/*
* If both a send and receive test are configured, then were testing the
* throughput (round trip delivery) of mail. This can be configured to
* send and receive to the same of different hosts.
*/
if (mailParms.getSendTest() != null && mailParms.getReadTest() != null) {
/*
* Doing round-trip mail test so create a unique subject for
* matching.
*/
mailParms.setEnd2EndTestInProgress(true);
status = sendTestMessage(mailParms);
if (status.isAvailable()) {
LogUtils.debugf(this, "doMailTest: send test successful.");
status = readTestMessage(mailParms);
} else {
LogUtils.infof(this, "doMailTest: send test unsuccessful... skipping read portion of test.");
}
} else if (mailParms.getReadTest() != null) {
status = readTestMessage(mailParms);
} else if (mailParms.getSendTest() != null) {
status = sendTestMessage(mailParms);
} else {
throw new IllegalArgumentException("MailTransportMonitor requires either send-host or read-host parameters");
}
if (status.isAvailable()) {
status.setResponseTime(Double.valueOf(String.valueOf(System.currentTimeMillis() - beginPoll)));
}
LogUtils.infof(this, "doMailTest: mailtest result: %s", status);
return status;
}
private PollStatus readTestMessage(final MailTransportParameters mailParms) {
LogUtils.debugf(this, "readTestMessage: Beginning read mail test.");
PollStatus status = PollStatus.unavailable("Test not completed.");
final long interval = mailParms.getReadTestAttemptInterval();
if (mailParms.isEnd2EndTestInProgress()) {
LogUtils.debugf(this, "Initially delaying read test: %d because end to end test is in progress.", mailParms.getReadTestAttemptInterval());
if (delayTest(status, interval) == PollStatus.SERVICE_UNKNOWN) {
return status;
}
}
Store mailStore = null;
Folder mailFolder = null;
try {
final JavaMailer readMailer = new JavaMailer(mailParms.getJavamailProperties());
setReadMailProperties(mailParms, readMailer);
final TimeoutTracker tracker = new TimeoutTracker(mailParms.getParameterMap(), mailParms.getRetries(), mailParms.getTimeout());
for (tracker.reset(); tracker.shouldRetry(); tracker.nextAttempt()) {
tracker.startAttempt();
if (tracker.getAttempt() > 0) {
if (delayTest(status, interval) == PollStatus.SERVICE_UNKNOWN) {
LogUtils.warnf(this, "readTestMessage: Status set to: %s during delay, exiting test.", status);
break;
}
}
LogUtils.debugf(this, "readTestMessage: reading mail attempt: %d, elapsed time: %.2fms.", (tracker.getAttempt()+1), tracker.elapsedTimeInMillis());
try {
mailStore = readMailer.getSession().getStore();
mailFolder = retrieveMailFolder(mailParms, mailStore);
mailFolder.open(Folder.READ_WRITE);
} catch (final MessagingException e) {
if (tracker.shouldRetry()) {
LogUtils.warnf(this, e, "readTestMessage: error reading INBOX");
closeStore(mailStore, mailFolder);
continue; //try again to get mail Folder from Store
} else {
LogUtils.warnf(this, e, "readTestMessage: error reading INBOX");
return PollStatus.down(e.getLocalizedMessage());
}
}
if (mailFolder.isOpen() && (mailParms.getReadTest().getSubjectMatch() != null || mailParms.isEnd2EndTestInProgress())) {
status = processMailSubject(mailParms, mailFolder);
if (status.getStatusCode() == PollStatus.SERVICE_AVAILABLE) {
break;
}
}
}
} catch (final JavaMailerException e) {
status = PollStatus.down(e.getLocalizedMessage());
} finally {
closeStore(mailStore, mailFolder);
}
return status;
}
/**
* Handy method to do the try catch try of closing a mail store and folder.
* @param mailStore
* @param mailFolder
*/
private void closeStore(final Store mailStore, final Folder mailFolder) {
try {
if (mailFolder != null && mailFolder.isOpen()) {
mailFolder.close(true);
}
} catch (final MessagingException e) {
LogUtils.debugf(this, e, "Unable to close mail folder.");
} finally {
try {
if (mailStore != null && mailStore.isConnected()) {
mailStore.close();
}
} catch (final MessagingException e1) {
LogUtils.debugf(this, e1, "Unable to close message store.");
}
}
}
/**
* After a mailbox has been opened, search through the retrieved messages
* for a matching subject.
*
* @param mailParms
* @param mailFolder
* @return a PollStatus indicative of the success of matching a subject or just retieving
* mail folder contents... dependent on configuration.
*/
private PollStatus processMailSubject(final MailTransportParameters mailParms, final Folder mailFolder) {
PollStatus status = PollStatus.unknown();
try {
final String subject = computeMatchingSubject(mailParms);
if (mailFolder.isOpen() && subject != null) {
final Message[] mailMessages = mailFolder.getMessages();
final SearchTerm searchTerm = new SubjectTerm(subject);
final SearchTerm deleteTerm = new HeaderTerm(MTM_HEADER_KEY, m_headerValue);
LogUtils.debugf(this, "searchMailSubject: searching %d message(s) for subject '%s'", mailMessages.length, subject);
boolean delete = false;
boolean found = false;
for (int i = 1; i <= mailMessages.length; i++) {
final Message mailMessage = mailFolder.getMessage(i);
LogUtils.debugf(this, "searchMailSubject: retrieved message subject '%s'", mailMessage.getSubject());
if (mailMessage.match(searchTerm)) {
found = true;
LogUtils.debugf(this, "searchMailSubject: message with subject '%s' found.", subject);
if (mailParms.isEnd2EndTestInProgress()) {
if (!delete) LogUtils.debugf(this, "searchMailSubject: flagging message with subject '%s' for deletion for end2end test.", subject);
delete = true;
}
}
final boolean deleteAllMail = mailParms.getReadTest().isDeleteAllMail();
final boolean foundMTMHeader = mailMessage.match(deleteTerm);
LogUtils.debugf(this, "searchMailSubject: deleteAllMail = %s, MTM header found = %s", Boolean.toString(deleteAllMail), Boolean.toString(foundMTMHeader));
if (deleteAllMail) {
if (!delete) LogUtils.debugf(this, "searchMailSubject: flagging message with subject '%s' for deletion because deleteAllMail is set.", subject);
delete = true;
} else if (foundMTMHeader) {
if (!delete) LogUtils.debugf(this, "searchMailSubject: flagging message with subject '%s' for deletion because we sent it (found header %s=%s)", subject, MTM_HEADER_KEY, m_headerValue);
delete = true;
}
if (delete) {
mailMessage.setFlag(Flag.DELETED, true);
}
// since we want to delete old messages matchin MTM_HEADER_KEY, we can't break early
// if (found) break;
}
if (!found) {
LogUtils.debugf(this, "searchMailSubject: message with subject: '%s' NOT found.", subject);
status = PollStatus.down("searchMailSubject: matching test message: '"+subject+"', not found.");
} else {
status = PollStatus.available();
}
}
} catch (final MessagingException e) {
return PollStatus.down(e.getLocalizedMessage());
}
return status;
}
/**
* An end2end test has the subject appended with a unique value for matching the read with the send.
*
* @param mailParms
* @return a computed subject based on the requirements of the service configuration.
*/
private String computeMatchingSubject(final MailTransportParameters mailParms) {
String subject = null;
if (mailParms.isEnd2EndTestInProgress()) {
subject = mailParms.getComputedTestSubject();
} else {
subject = mailParms.getReadTest().getSubjectMatch();
}
return subject;
}
/**
* This sets up the properties for the read mail portion of the service poll. These properties
* are derived from configuration elements vs. being hardcoded javamail properties. Elements
* that conflict with javamail defined properties always win.
*
* @param mailParms
* @param readMailer
*/
private void setReadMailProperties(final MailTransportParameters mailParms, final JavaMailer readMailer) {
final Properties sendMailProps = readMailer.getSession().getProperties();
final String protocol = mailParms.getReadTestProtocol();
sendMailProps.put("mail." + protocol + ".host", mailParms.getReadTestHost());
sendMailProps.put("mail." + protocol + ".user", mailParms.getReadTestUserName());
sendMailProps.put("mail." + protocol + ".port", mailParms.getReadTestPort());
sendMailProps.put("mail." + protocol + ".starttls.enable", mailParms.isReadTestStartTlsEnabled());
sendMailProps.put("mail.smtp.auth", "true");
if (mailParms.isReadTestSslEnabled()) {
sendMailProps.put("mail." + protocol + ".socketFactory.port", mailParms.getReadTestPort());
sendMailProps.put("mail." + protocol + ".socketFactory.class", "javax.net.ssl.SSLSocketFactory");
sendMailProps.put("mail." + protocol + ".socketFactory.fallback", "false");
}
sendMailProps.put("mail." + protocol + ".connectiontimeout", mailParms.getTimeout());
sendMailProps.put("mail." + protocol + ".timeout", mailParms.getTimeout());
sendMailProps.put("mail.store.protocol", protocol);
}
/**
* Establish connection with mail store and return the configured mail folder.
*
* @param mailParms
* @param mailStore
* @return the folder specified in configuration
* @throws MessagingException
*/
private Folder retrieveMailFolder(final MailTransportParameters mailParms, final Store mailStore) throws MessagingException {
mailStore.connect(mailParms.getReadTestHost(), mailParms.getReadTestPort(), mailParms.getReadTestUserName(), mailParms.getReadTestPassword());
Folder mailFolder = mailStore.getDefaultFolder();
mailFolder = mailFolder.getFolder(mailParms.getReadTestFolder());
return mailFolder;
}
/**
* Sends message based on properties and fields configured for the service.
*
* @param mailParms
* @return a PollStatus
*/
private PollStatus sendTestMessage(final MailTransportParameters mailParms) {
PollStatus status = PollStatus.unavailable("Test not completed.");
final long interval = mailParms.getSendTestAttemptInterval();
final TimeoutTracker tracker = new TimeoutTracker(mailParms.getParameterMap(), mailParms.getRetries(), mailParms.getTimeout());
for (tracker.reset(); tracker.shouldRetry(); tracker.nextAttempt()) {
tracker.startAttempt();
LogUtils.debugf(this, "sendTestMessage: sending mail attempt: %d, elapsed time: %.2fms", (tracker.getAttempt() + 1), tracker.elapsedTimeInMillis());
try {
final JavaMailer sendMailer = createMailer(mailParms);
overRideDefaultProperties(mailParms, sendMailer);
sendMailer.mailSend();
status = PollStatus.available();
break;
} catch (final JavaMailerException e) {
status = PollStatus.unavailable(e.getLocalizedMessage());
}
if (tracker.shouldRetry()) {
delayTest(status, interval);
}
}
return status;
}
/**
* Set poll status to unknown indicate an exception occurred while attempting to sleep the thread.
* @param status
* @param interval
* @return returns an unchanged PollStatus unless an exception happens in which case status is changed to unknown.
*/
private int delayTest(PollStatus status, final long interval) {
LogUtils.debugf(this, "delayTest: delaying test for: %dms. per configuration.", interval);
try {
Thread.sleep(interval);
} catch (final InterruptedException e) {
LogUtils.errorf(this, e, "delayTest: An interrupt exception occurred while delaying the mail test");
status = PollStatus.unknown(e.getLocalizedMessage());
}
return status.getStatusCode();
}
/**
* Override some fields in the JavaMailer class. TODO: This needs
* re-factoring!
* @param mailParms
* @param sendMailer
*/
private void overRideDefaultProperties(final MailTransportParameters mailParms, final JavaMailer sendMailer) {
sendMailer.setFrom(mailParms.getSendTestFrom());
sendMailer.getSession().setDebug(mailParms.isSendTestDebug());
sendMailer.setDebug(mailParms.isSendTestDebug());
sendMailer.setEncoding(mailParms.getSendTestMessageEncoding());
sendMailer.setMailer(mailParms.getSendTestMailer());
sendMailer.setMailHost(mailParms.getSendTestHost());
//char_set, encoding, m_contentType
sendMailer.setMessageText(mailParms.getSendTestMessageBody());
sendMailer.setCharSet(mailParms.getSendTestCharSet());
sendMailer.setContentType(mailParms.getSendTestMessageContentType());
sendMailer.setSmtpSsl(mailParms.isSendTestIsSslEnable());
sendMailer.setSubject(mailParms.getComputedTestSubject());
sendMailer.setTo(mailParms.getSendTestRecipeint());
sendMailer.setTransport(mailParms.getSendTestTransport());
sendMailer.setUseJMTA(mailParms.isSendTestUseJmta());
}
private JavaMailer createMailer(final MailTransportParameters mailParms) throws JavaMailerException {
final JavaMailer sendMailer = new JavaMailer(mailParms.getJavamailProperties());
final String mailPropsPrefix = new StringBuilder("mail.").append(mailParms.getSendTestTransport()).append('.').toString();
final Properties props = sendMailer.getSession().getProperties();
//user
props.setProperty(mailPropsPrefix+"user", mailParms.getSendTestUserName());
sendMailer.setUser(mailParms.getSendTestUserName());
sendMailer.setPassword(mailParms.getSendTestPassword());
//host
props.setProperty(mailPropsPrefix+"host", mailParms.getSendTestHost());
sendMailer.setMailHost(mailParms.getSendTestHost());
//port
props.setProperty(mailPropsPrefix+"port", String.valueOf(mailParms.getSendTestPort()));
sendMailer.setSmtpPort(mailParms.getSendTestPort());
//connectiontimeout
//Override this with configured javamail property because this setting is a generic timeout value
if (!props.containsKey(mailPropsPrefix+"connectiontimeout")) {
props.setProperty(mailPropsPrefix+"connectiontimeout", String.valueOf(mailParms.getTimeout()));
}
//timeout
//Override this with configured javamail property because this setting is a generic timeout value
if (!props.containsKey(mailPropsPrefix+"timeout")) {
props.setProperty(mailPropsPrefix+"timeout", String.valueOf(mailParms.getTimeout()));
}
//from
props.setProperty(mailPropsPrefix+"from", mailParms.getSendTestFrom());
sendMailer.setFrom(mailParms.getSendTestFrom());
//auth
props.setProperty(mailPropsPrefix+"auth", String.valueOf(mailParms.isSendTestUseAuth()));
sendMailer.setAuthenticate(mailParms.isSendTestUseAuth());
//quitwait
props.setProperty(mailPropsPrefix+"quitwait", String.valueOf(mailParms.isSendTestIsQuitWait()));
sendMailer.setQuitWait(mailParms.isSendTestIsQuitWait());
//socketFactory.class
//socketFactory.port
if (mailParms.isSendTestIsSslEnable()) {
//override this hard coded default if this property is specified
if (!props.containsKey(mailPropsPrefix+"socketFactory.class")) {
props.setProperty(mailPropsPrefix+"socketFactory.class", "javax.net.ssl.SSLSocketFactory");
}
props.setProperty(mailPropsPrefix+"socketFactory.port", String.valueOf(mailParms.getSendTestPort()));
sendMailer.setSmtpPort(mailParms.getSendTestPort());
}
sendMailer.setSmtpSsl(mailParms.isSendTestIsSslEnable());
//starttls.enable
props.setProperty(mailPropsPrefix+"starttls.enable", String.valueOf(mailParms.isSendTestStartTls()));
sendMailer.setStartTlsEnabled(mailParms.isSendTestStartTls());
sendMailer.addExtraHeader(MTM_HEADER_KEY, m_headerValue);
sendMailer.setSession(Session.getInstance(props, sendMailer.createAuthenticator()));
return sendMailer;
}
}