/**
* Logback: the reliable, generic, fast and flexible logging framework.
* Copyright (C) 1999-2015, QOS.ch. All rights reserved.
*
* This program and the accompanying materials are dual-licensed under
* either the terms of the Eclipse Public License v1.0 as published by
* the Eclipse Foundation
*
* or (per the licensee's choosing)
*
* under the terms of the GNU Lesser General Public License version 2.1
* as published by the Free Software Foundation.
*/
package ch.qos.logback.core.net;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Properties;
import javax.mail.Message;
import javax.mail.Multipart;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import javax.naming.Context;
import javax.naming.InitialContext;
import ch.qos.logback.core.AppenderBase;
import ch.qos.logback.core.CoreConstants;
import ch.qos.logback.core.Layout;
import ch.qos.logback.core.boolex.EvaluationException;
import ch.qos.logback.core.boolex.EventEvaluator;
import ch.qos.logback.core.helpers.CyclicBuffer;
import ch.qos.logback.core.pattern.PatternLayoutBase;
import ch.qos.logback.core.sift.DefaultDiscriminator;
import ch.qos.logback.core.sift.Discriminator;
import ch.qos.logback.core.spi.CyclicBufferTracker;
import ch.qos.logback.core.util.ContentTypeUtil;
import ch.qos.logback.core.util.OptionHelper;
// Contributors:
// Andrey Rybin charset encoding support http://jira.qos.ch/browse/LBCORE-69
/**
* An abstract class that provides support for sending events to an email address.
* <p/>
* <p/>
* See http://logback.qos.ch/manual/appenders.html#SMTPAppender for further documentation.
*
* @author Ceki Gülcü
* @author Sébastien Pennec
*/
public abstract class SMTPAppenderBase<E> extends AppenderBase<E> {
static InternetAddress[] EMPTY_IA_ARRAY = new InternetAddress[0];
// ~ 14 days
static final int MAX_DELAY_BETWEEN_STATUS_MESSAGES = 1228800 * CoreConstants.MILLIS_IN_ONE_SECOND;
long lastTrackerStatusPrint = 0;
int delayBetweenStatusMessages = 300 * CoreConstants.MILLIS_IN_ONE_SECOND;
protected Layout<E> subjectLayout;
protected Layout<E> layout;
private List<PatternLayoutBase<E>> toPatternLayoutList = new ArrayList<PatternLayoutBase<E>>();
private String from;
private String subjectStr = null;
private String smtpHost;
private int smtpPort = 25;
private boolean starttls = false;
private boolean ssl = false;
private boolean sessionViaJNDI = false;
private String jndiLocation = CoreConstants.JNDI_COMP_PREFIX
+ "/mail/Session";
String username;
String password;
String localhost;
boolean asynchronousSending = true;
private String charsetEncoding = "UTF-8";
protected Session session;
protected EventEvaluator<E> eventEvaluator;
protected Discriminator<E> discriminator = new DefaultDiscriminator<E>();
protected CyclicBufferTracker<E> cbTracker;
private int errorCount = 0;
/**
* return a layout for the subject string as appropriate for the module. If the subjectStr parameter is null, then a
* default value for subjectStr should be used.
*
* @param subjectStr
* @return a layout as appropriate for the module
*/
protected abstract Layout<E> makeSubjectLayout(String subjectStr);
/**
* Start the appender
*/
public void start() {
if (cbTracker == null) {
cbTracker = new CyclicBufferTracker<E>();
}
if (sessionViaJNDI) {
session = lookupSessionInJNDI();
} else {
session = buildSessionFromProperties();
}
if (session == null) {
addError("Failed to obtain javax.mail.Session. Cannot start.");
return;
}
subjectLayout = makeSubjectLayout(subjectStr);
started = true;
}
private Session lookupSessionInJNDI() {
addInfo("Looking up javax.mail.Session at JNDI location ["
+ jndiLocation + "]");
try {
Context initialContext = new InitialContext();
Object obj = initialContext.lookup(jndiLocation);
return (Session) obj;
} catch (Exception e) {
addError("Failed to obtain javax.mail.Session from JNDI location ["
+ jndiLocation + "]");
return null;
}
}
private Session buildSessionFromProperties() {
Properties props = new Properties(OptionHelper.getSystemProperties());
if (smtpHost != null) {
props.put("mail.smtp.host", smtpHost);
}
props.put("mail.smtp.port", Integer.toString(smtpPort));
if (localhost != null) {
props.put("mail.smtp.localhost", localhost);
}
LoginAuthenticator loginAuthenticator = null;
if (username != null) {
loginAuthenticator = new LoginAuthenticator(username, password);
props.put("mail.smtp.auth", "true");
}
if (isSTARTTLS() && isSSL()) {
addError("Both SSL and StartTLS cannot be enabled simultaneously");
} else {
if (isSTARTTLS()) {
// see also http://jira.qos.ch/browse/LBCORE-225
props.put("mail.smtp.starttls.enable", "true");
}
if (isSSL()) {
String SSL_FACTORY = "javax.net.ssl.SSLSocketFactory";
props.put("mail.smtp.socketFactory.port",
Integer.toString(smtpPort));
props.put("mail.smtp.socketFactory.class", SSL_FACTORY);
props.put("mail.smtp.socketFactory.fallback", "true");
props.put("mail.smtp.ssl.enable", "true");
props.put("mail.transport.protocol", "smtps");
props.put("mail.smtps.ssl.trust", "*");
}
}
// props.put("mail.debug", "true");
return Session.getInstance(props, loginAuthenticator);
}
/**
* Perform SMTPAppender specific appending actions, delegating some of them to a subclass and checking if the event
* triggers an e-mail to be sent.
*/
protected void append(E eventObject) {
if (!checkEntryConditions()) {
return;
}
String key = discriminator.getDiscriminatingValue(eventObject);
long now = System.currentTimeMillis();
final CyclicBuffer<E> cb = cbTracker.getOrCreate(key, now);
subAppend(cb, eventObject);
try {
if (eventEvaluator.evaluate(eventObject)) {
// clone the CyclicBuffer before sending out asynchronously
CyclicBuffer<E> cbClone = new CyclicBuffer<E>(cb);
// see http://jira.qos.ch/browse/LBCLASSIC-221
cb.clear();
if (asynchronousSending) {
// perform actual sending asynchronously
SenderRunnable senderRunnable = new SenderRunnable(cbClone,
eventObject);
context.getExecutorService().execute(senderRunnable);
} else {
// synchronous sending
sendBuffer(cbClone, eventObject);
}
}
} catch (EvaluationException ex) {
errorCount++;
if (errorCount < CoreConstants.MAX_ERROR_COUNT) {
addError("SMTPAppender's EventEvaluator threw an Exception-",
ex);
}
}
// immediately remove the buffer if asked by the user
if (eventMarksEndOfLife(eventObject)) {
cbTracker.endOfLife(key);
}
cbTracker.removeStaleComponents(now);
if ((lastTrackerStatusPrint + delayBetweenStatusMessages) < now) {
addInfo("SMTPAppender [" + name + "] is tracking ["
+ cbTracker.getComponentCount() + "] buffers");
lastTrackerStatusPrint = now;
// quadruple 'delay' assuming less than max delay
if (delayBetweenStatusMessages < MAX_DELAY_BETWEEN_STATUS_MESSAGES) {
delayBetweenStatusMessages *= 4;
}
}
}
protected abstract boolean eventMarksEndOfLife(E eventObject);
protected abstract void subAppend(CyclicBuffer<E> cb, E eventObject);
/**
* This method determines if there is a sense in attempting to append.
* <p/>
* <p/>
* It checks whether there is a set output target and also if there is a set layout. If these checks fail, then the
* boolean value <code>false</code> is returned.
*/
public boolean checkEntryConditions() {
if (!this.started) {
addError("Attempting to append to a non-started appender: "
+ this.getName());
return false;
}
if (this.eventEvaluator == null) {
addError("No EventEvaluator is set for appender [" + name + "].");
return false;
}
if (this.layout == null) {
addError("No layout set for appender named ["
+ name
+ "]. For more information, please visit http://logback.qos.ch/codes.html#smtp_no_layout");
return false;
}
return true;
}
public synchronized void stop() {
this.started = false;
}
InternetAddress getAddress(String addressStr) {
try {
return new InternetAddress(addressStr);
} catch (AddressException e) {
addError("Could not parse address [" + addressStr + "].", e);
return null;
}
}
private List<InternetAddress> parseAddress(E event) {
int len = toPatternLayoutList.size();
List<InternetAddress> iaList = new ArrayList<InternetAddress>();
for (int i = 0; i < len; i++) {
try {
PatternLayoutBase<E> emailPL = toPatternLayoutList.get(i);
String emailAdrr = emailPL.doLayout(event);
if ((emailAdrr == null) || (emailAdrr.length() == 0)) {
continue;
}
InternetAddress[] tmp = InternetAddress.parse(emailAdrr, true);
iaList.addAll(Arrays.asList(tmp));
} catch (AddressException e) {
addError("Could not parse email address for ["
+ toPatternLayoutList.get(i) + "] for event [" + event
+ "]", e);
return iaList;
}
}
return iaList;
}
/**
* Returns value of the <b>toList</b> option.
*/
public List<PatternLayoutBase<E>> getToList() {
return toPatternLayoutList;
}
/**
* Send the contents of the cyclic buffer as an e-mail message.
*/
protected void sendBuffer(CyclicBuffer<E> cb, E lastEventObject) {
// Note: this code already owns the monitor for this
// appender. This frees us from needing to synchronize on 'cb'.
try {
MimeBodyPart part = new MimeBodyPart();
StringBuffer sbuf = new StringBuffer();
String header = layout.getFileHeader();
if (header != null) {
sbuf.append(header);
}
String presentationHeader = layout.getPresentationHeader();
if (presentationHeader != null) {
sbuf.append(presentationHeader);
}
fillBuffer(cb, sbuf);
String presentationFooter = layout.getPresentationFooter();
if (presentationFooter != null) {
sbuf.append(presentationFooter);
}
String footer = layout.getFileFooter();
if (footer != null) {
sbuf.append(footer);
}
String subjectStr = "Undefined subject";
if (subjectLayout != null) {
subjectStr = subjectLayout.doLayout(lastEventObject);
// The subject must not contain new-line characters, which cause
// an SMTP error (LOGBACK-865). Truncate the string at the first
// new-line character.
int newLinePos = (subjectStr != null) ? subjectStr
.indexOf('\n') : (-1);
if (newLinePos > -1) {
subjectStr = subjectStr.substring(0, newLinePos);
}
}
MimeMessage mimeMsg = new MimeMessage(session);
if (from != null) {
mimeMsg.setFrom(getAddress(from));
} else {
mimeMsg.setFrom();
}
mimeMsg.setSubject(subjectStr, charsetEncoding);
List<InternetAddress> destinationAddresses = parseAddress(lastEventObject);
if (destinationAddresses.isEmpty()) {
addInfo("Empty destination address. Aborting email transmission");
return;
}
InternetAddress[] toAddressArray = destinationAddresses
.toArray(EMPTY_IA_ARRAY);
mimeMsg.setRecipients(Message.RecipientType.TO, toAddressArray);
String contentType = layout.getContentType();
if (ContentTypeUtil.isTextual(contentType)) {
part.setText(sbuf.toString(), charsetEncoding,
ContentTypeUtil.getSubType(contentType));
} else {
part.setContent(sbuf.toString(), layout.getContentType());
}
Multipart mp = new MimeMultipart();
mp.addBodyPart(part);
mimeMsg.setContent(mp);
mimeMsg.setSentDate(new Date());
addInfo("About to send out SMTP message \"" + subjectStr + "\" to "
+ Arrays.toString(toAddressArray));
// Transport.send(mimeMsg);
Transport transport = session.getTransport(session
.getProperty("mail.transport.protocol"));
transport.connect(smtpHost, smtpPort, username, password);
transport.sendMessage(mimeMsg, mimeMsg.getAllRecipients());
transport.close();
} catch (Exception e) {
addError("Error occurred while sending e-mail notification.", e);
}
}
protected abstract void fillBuffer(CyclicBuffer<E> cb, StringBuffer sbuf);
/**
* Returns value of the <b>From</b> option.
*/
public String getFrom() {
return from;
}
/**
* Returns value of the <b>Subject</b> option.
*/
public String getSubject() {
return subjectStr;
}
/**
* The <b>From</b> option takes a string value which should be a e-mail address of the sender.
*/
public void setFrom(String from) {
this.from = from;
}
/**
* The <b>Subject</b> option takes a string value which should be a the subject of the e-mail message.
*/
public void setSubject(String subject) {
this.subjectStr = subject;
}
/**
* Alias for smtpHost
*
* @param smtpHost
*/
public void setSMTPHost(String smtpHost) {
setSmtpHost(smtpHost);
}
/**
* The <b>smtpHost</b> option takes a string value which should be a the host name of the SMTP server that will send
* the e-mail message.
*/
public void setSmtpHost(String smtpHost) {
this.smtpHost = smtpHost;
}
/**
* Alias for getSmtpHost().
*/
public String getSMTPHost() {
return getSmtpHost();
}
/**
* Returns value of the <b>SMTPHost</b> option.
*/
public String getSmtpHost() {
return smtpHost;
}
/**
* Alias for {@link #setSmtpPort}.
*
* @param port
*/
public void setSMTPPort(int port) {
setSmtpPort(port);
}
/**
* The port where the SMTP server is running. Default value is 25.
*
* @param port
*/
public void setSmtpPort(int port) {
this.smtpPort = port;
}
/**
* Alias for {@link #getSmtpPort}
*
* @return
*/
public int getSMTPPort() {
return getSmtpPort();
}
/**
* See {@link #setSmtpPort}
*
* @return
*/
public int getSmtpPort() {
return smtpPort;
}
public String getLocalhost() {
return localhost;
}
/**
* Set the "mail.smtp.localhost" property to the value passed as parameter to this method.
* <p/>
* <p>
* Useful in case the hostname for the client host is not fully qualified and as a consequence the SMTP server
* rejects the clients HELO/EHLO command.
* </p>
*
* @param localhost
*/
public void setLocalhost(String localhost) {
this.localhost = localhost;
}
public CyclicBufferTracker<E> getCyclicBufferTracker() {
return cbTracker;
}
public void setCyclicBufferTracker(CyclicBufferTracker<E> cbTracker) {
this.cbTracker = cbTracker;
}
public Discriminator<E> getDiscriminator() {
return discriminator;
}
public void setDiscriminator(Discriminator<E> discriminator) {
this.discriminator = discriminator;
}
public boolean isAsynchronousSending() {
return asynchronousSending;
}
/**
* By default, SMTAppender transmits emails asynchronously. For synchronous email transmission set
* asynchronousSending to 'false'.
*
* @param asynchronousSending
* determines whether sending is done asynchronously or not
* @since 1.0.4
*/
public void setAsynchronousSending(boolean asynchronousSending) {
this.asynchronousSending = asynchronousSending;
}
public void addTo(String to) {
if ((to == null) || (to.length() == 0)) {
throw new IllegalArgumentException("Null or empty <to> property");
}
PatternLayoutBase plb = makeNewToPatternLayout(to.trim());
plb.setContext(context);
plb.start();
this.toPatternLayoutList.add(plb);
}
protected abstract PatternLayoutBase<E> makeNewToPatternLayout(
String toPattern);
public List<String> getToAsListOfString() {
List<String> toList = new ArrayList<String>();
for (PatternLayoutBase plb : toPatternLayoutList) {
toList.add(plb.getPattern());
}
return toList;
}
public boolean isSTARTTLS() {
return starttls;
}
public void setSTARTTLS(boolean startTLS) {
this.starttls = startTLS;
}
public boolean isSSL() {
return ssl;
}
public void setSSL(boolean ssl) {
this.ssl = ssl;
}
/**
* The <b>EventEvaluator</b> option takes a string value representing the name of the class implementing the
* {@link EventEvaluator} interface. A corresponding object will be instantiated and assigned as the event evaluator
* for the SMTPAppender.
*/
public void setEvaluator(EventEvaluator<E> eventEvaluator) {
this.eventEvaluator = eventEvaluator;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
/**
* @return the charset encoding value
* @see #setCharsetEncoding(String)
*/
public String getCharsetEncoding() {
return charsetEncoding;
}
public String getJndiLocation() {
return jndiLocation;
}
/**
* Set the location where a {@link javax.mail.Session} resource is located in JNDI. Default value is
* "java:comp/env/mail/Session".
*
* @param jndiLocation
* @since 1.0.6
*/
public void setJndiLocation(String jndiLocation) {
this.jndiLocation = jndiLocation;
}
public boolean isSessionViaJNDI() {
return sessionViaJNDI;
}
/**
* If set to true, a {@link javax.mail.Session} resource will be retrieved from JNDI. Default is false.
*
* @param sessionViaJNDI
* whether to obtain a javax.mail.Session by JNDI
* @since 1.0.6
*/
public void setSessionViaJNDI(boolean sessionViaJNDI) {
this.sessionViaJNDI = sessionViaJNDI;
}
/**
* Set the character set encoding of the outgoing email messages. The default encoding is "UTF-8" which usually
* works well for most purposes.
*
* @param charsetEncoding
*/
public void setCharsetEncoding(String charsetEncoding) {
this.charsetEncoding = charsetEncoding;
}
public Layout<E> getLayout() {
return layout;
}
public void setLayout(Layout<E> layout) {
this.layout = layout;
}
class SenderRunnable implements Runnable {
final CyclicBuffer<E> cyclicBuffer;
final E e;
SenderRunnable(CyclicBuffer<E> cyclicBuffer, E e) {
this.cyclicBuffer = cyclicBuffer;
this.e = e;
}
public void run() {
sendBuffer(cyclicBuffer, e);
}
}
}