2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
9 * http://www.apache.org/licenses/LICENSE-2.0
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
18 package org.apache.log4j.net;
20 import org.apache.log4j.AppenderSkeleton;
21 import org.apache.log4j.Layout;
22 import org.apache.log4j.Level;
23 import org.apache.log4j.helpers.CyclicBuffer;
24 import org.apache.log4j.helpers.LogLog;
25 import org.apache.log4j.helpers.OptionConverter;
26 import org.apache.log4j.spi.ErrorCode;
27 import org.apache.log4j.spi.LoggingEvent;
28 import org.apache.log4j.spi.OptionHandler;
29 import org.apache.log4j.spi.TriggeringEventEvaluator;
30 import org.apache.log4j.xml.UnrecognizedElementHandler;
31 import org.w3c.dom.Element;
33 import javax.mail.Authenticator;
34 import javax.mail.Message;
35 import javax.mail.MessagingException;
36 import javax.mail.Multipart;
37 import javax.mail.PasswordAuthentication;
38 import javax.mail.Session;
39 import javax.mail.Transport;
40 import javax.mail.internet.AddressException;
41 import javax.mail.internet.InternetAddress;
42 import javax.mail.internet.InternetHeaders;
43 import javax.mail.internet.MimeBodyPart;
44 import javax.mail.internet.MimeMessage;
45 import javax.mail.internet.MimeMultipart;
46 import javax.mail.internet.MimeUtility;
47 import java.io.ByteArrayOutputStream;
48 import java.io.OutputStreamWriter;
49 import java.io.UnsupportedEncodingException;
50 import java.io.Writer;
51 import java.util.Date;
52 import java.util.Properties;
55 Send an e-mail when a specific logging event occurs, typically on
56 errors or fatal errors.
58 <p>The number of logging events delivered in this e-mail depend on
59 the value of <b>BufferSize</b> option. The
60 <code>SMTPAppender</code> keeps only the last
61 <code>BufferSize</code> logging events in its cyclic buffer. This
62 keeps memory requirements at a reasonable level while still
63 delivering useful application context.
65 By default, an email message will be sent when an ERROR or higher
66 severity message is appended. The triggering criteria can be
67 modified by setting the evaluatorClass property with the name
68 of a class implementing TriggeringEventEvaluator, setting the evaluator
69 property with an instance of TriggeringEventEvaluator or
70 nesting a triggeringPolicy element where the specified
71 class implements TriggeringEventEvaluator.
73 This class has implemented UnrecognizedElementHandler since 1.2.15.
75 Since 1.2.16, SMTP over SSL is supported by setting SMTPProtocol to "smpts".
77 @author Ceki Gülcü
79 public class SMTPAppender extends AppenderSkeleton
80 implements UnrecognizedElementHandler {
83 * Comma separated list of cc recipients.
87 * Comma separated list of bcc recipients.
92 * Comma separated list of replyTo addresses.
94 private String replyTo;
95 private String subject;
96 private String smtpHost;
97 private String smtpUsername;
98 private String smtpPassword;
99 private String smtpProtocol;
100 private int smtpPort = -1;
101 private boolean smtpDebug = false;
102 private int bufferSize = 512;
103 private boolean locationInfo = false;
104 private boolean sendOnClose = false;
106 protected CyclicBuffer cb = new CyclicBuffer(bufferSize);
107 protected Message msg;
109 protected TriggeringEventEvaluator evaluator;
114 The default constructor will instantiate the appender with a
115 {@link TriggeringEventEvaluator} that will trigger on events with
116 level ERROR or higher.*/
119 this(new DefaultEvaluator());
124 Use <code>evaluator</code> passed as parameter as the {@link
125 TriggeringEventEvaluator} for this SMTPAppender. */
127 SMTPAppender(TriggeringEventEvaluator evaluator) {
128 this.evaluator = evaluator;
133 Activate the specified options, such as the smtp host, the
134 recipient, from, etc. */
136 void activateOptions() {
137 Session session = createSession();
138 msg = new MimeMessage(session);
142 if(subject != null) {
144 msg.setSubject(MimeUtility.encodeText(subject, "UTF-8", null));
145 } catch(UnsupportedEncodingException ex) {
146 LogLog.error("Unable to encode SMTP subject", ex);
149 } catch(MessagingException e) {
150 LogLog.error("Could not activate SMTPAppender options.", e );
153 if (evaluator instanceof OptionHandler) {
154 ((OptionHandler) evaluator).activateOptions();
160 * @param msg message, may not be null.
161 * @throws MessagingException thrown if error addressing message.
164 protected void addressMessage(final Message msg) throws MessagingException {
166 msg.setFrom(getAddress(from));
171 //Add ReplyTo addresses if defined.
172 if (replyTo != null && replyTo.length() > 0) {
173 msg.setReplyTo(parseAddress(replyTo));
176 if (to != null && to.length() > 0) {
177 msg.setRecipients(Message.RecipientType.TO, parseAddress(to));
180 //Add CC receipients if defined.
181 if (cc != null && cc.length() > 0) {
182 msg.setRecipients(Message.RecipientType.CC, parseAddress(cc));
185 //Add BCC receipients if defined.
186 if (bcc != null && bcc.length() > 0) {
187 msg.setRecipients(Message.RecipientType.BCC, parseAddress(bcc));
192 * Create mail session.
193 * @return mail session, may not be null.
196 protected Session createSession() {
197 Properties props = null;
199 props = new Properties (System.getProperties());
200 } catch(SecurityException ex) {
201 props = new Properties();
204 String prefix = "mail.smtp";
205 if (smtpProtocol != null) {
206 props.put("mail.transport.protocol", smtpProtocol);
207 prefix = "mail." + smtpProtocol;
209 if (smtpHost != null) {
210 props.put(prefix + ".host", smtpHost);
213 props.put(prefix + ".port", String.valueOf(smtpPort));
216 Authenticator auth = null;
217 if(smtpPassword != null && smtpUsername != null) {
218 props.put(prefix + ".auth", "true");
219 auth = new Authenticator() {
220 protected PasswordAuthentication getPasswordAuthentication() {
221 return new PasswordAuthentication(smtpUsername, smtpPassword);
225 Session session = Session.getInstance(props, auth);
226 if (smtpProtocol != null) {
227 session.setProtocolForAddress("rfc822", smtpProtocol);
230 session.setDebug(smtpDebug);
236 Perform SMTPAppender specific appending actions, mainly adding
237 the event to a cyclic buffer and checking if the event triggers
238 an e-mail to be sent. */
240 void append(LoggingEvent event) {
242 if(!checkEntryConditions()) {
246 event.getThreadName();
250 event.getLocationInformation();
252 event.getRenderedMessage();
253 event.getThrowableStrRep();
255 if(evaluator.isTriggeringEvent(event)) {
261 This method determines if there is a sense in attempting to append.
263 <p>It checks whether there is a set output target and also if
264 there is a set layout. If these checks fail, then the boolean
265 value <code>false</code> is returned. */
267 boolean checkEntryConditions() {
268 if(this.msg == null) {
269 errorHandler.error("Message object not configured.");
273 if(this.evaluator == null) {
274 errorHandler.error("No TriggeringEventEvaluator is set for appender ["+
280 if(this.layout == null) {
281 errorHandler.error("No layout set for appender named ["+name+"].");
292 if (sendOnClose && cb.length() > 0) {
297 InternetAddress getAddress(String addressStr) {
299 return new InternetAddress(addressStr);
300 } catch(AddressException e) {
301 errorHandler.error("Could not parse address ["+addressStr+"].", e,
302 ErrorCode.ADDRESS_PARSE_FAILURE);
307 InternetAddress[] parseAddress(String addressStr) {
309 return InternetAddress.parse(addressStr, true);
310 } catch(AddressException e) {
311 errorHandler.error("Could not parse address ["+addressStr+"].", e,
312 ErrorCode.ADDRESS_PARSE_FAILURE);
318 Returns value of the <b>To</b> option.
327 The <code>SMTPAppender</code> requires a {@link
328 org.apache.log4j.Layout layout}. */
330 boolean requiresLayout() {
335 * Layout body of email message.
338 protected String formatBody() {
340 // Note: this code already owns the monitor for this
341 // appender. This frees us from needing to synchronize on 'cb'.
343 StringBuffer sbuf = new StringBuffer();
344 String t = layout.getHeader();
348 int len = cb.length();
349 for(int i = 0; i < len; i++) {
350 //sbuf.append(MimeUtility.encodeText(layout.format(cb.get())));
351 LoggingEvent event = cb.get();
352 sbuf.append(layout.format(event));
353 if(layout.ignoresThrowable()) {
354 String[] s = event.getThrowableStrRep();
356 for(int j = 0; j < s.length; j++) {
358 sbuf.append(Layout.LINE_SEP);
363 t = layout.getFooter();
368 return sbuf.toString();
372 Send the contents of the cyclic buffer as an e-mail message.
378 String s = formatBody();
379 boolean allAscii = true;
380 for(int i = 0; i < s.length() && allAscii; i++) {
381 allAscii = s.charAt(i) <= 0x7F;
385 part = new MimeBodyPart();
386 part.setContent(s, layout.getContentType());
389 ByteArrayOutputStream os = new ByteArrayOutputStream();
390 Writer writer = new OutputStreamWriter(
391 MimeUtility.encode(os, "quoted-printable"), "UTF-8");
394 InternetHeaders headers = new InternetHeaders();
395 headers.setHeader("Content-Type", layout.getContentType() + "; charset=UTF-8");
396 headers.setHeader("Content-Transfer-Encoding", "quoted-printable");
397 part = new MimeBodyPart(headers, os.toByteArray());
398 } catch(Exception ex) {
399 StringBuffer sbuf = new StringBuffer(s);
400 for (int i = 0; i < sbuf.length(); i++) {
401 if (sbuf.charAt(i) >= 0x80) {
402 sbuf.setCharAt(i, '?');
405 part = new MimeBodyPart();
406 part.setContent(sbuf.toString(), layout.getContentType());
412 Multipart mp = new MimeMultipart();
413 mp.addBodyPart(part);
416 msg.setSentDate(new Date());
418 } catch(MessagingException e) {
419 LogLog.error("Error occured while sending e-mail notification.", e);
420 } catch(RuntimeException e) {
421 LogLog.error("Error occured while sending e-mail notification.", e);
428 Returns value of the <b>EvaluatorClass</b> option.
431 String getEvaluatorClass() {
432 return evaluator == null ? null : evaluator.getClass().getName();
436 Returns value of the <b>From</b> option.
444 Get the reply addresses.
445 @return reply addresses as comma separated string, may be null.
449 String getReplyTo() {
454 Returns value of the <b>Subject</b> option.
457 String getSubject() {
462 The <b>From</b> option takes a string value which should be a
463 e-mail address of the sender.
466 void setFrom(String from) {
471 Set the e-mail addresses to which replies should be directed.
472 @param addresses reply addresses as comma separated string, may be null.
476 void setReplyTo(final String addresses) {
477 this.replyTo = addresses;
482 The <b>Subject</b> option takes a string value which should be a
483 the subject of the e-mail message.
486 void setSubject(String subject) {
487 this.subject = subject;
492 The <b>BufferSize</b> option takes a positive integer
493 representing the maximum number of logging events to collect in a
494 cyclic buffer. When the <code>BufferSize</code> is reached,
495 oldest events are deleted as new events are added to the
496 buffer. By default the size of the cyclic buffer is 512 events.
499 void setBufferSize(int bufferSize) {
500 this.bufferSize = bufferSize;
501 cb.resize(bufferSize);
505 The <b>SMTPHost</b> option takes a string value which should be a
506 the host name of the SMTP server that will send the e-mail message.
509 void setSMTPHost(String smtpHost) {
510 this.smtpHost = smtpHost;
514 Returns value of the <b>SMTPHost</b> option.
517 String getSMTPHost() {
522 The <b>To</b> option takes a string value which should be a
523 comma separated list of e-mail address of the recipients.
526 void setTo(String to) {
533 Returns value of the <b>BufferSize</b> option.
536 int getBufferSize() {
541 The <b>EvaluatorClass</b> option takes a string value
542 representing the name of the class implementing the {@link
543 TriggeringEventEvaluator} interface. A corresponding object will
544 be instantiated and assigned as the triggering event evaluator
545 for the SMTPAppender.
548 void setEvaluatorClass(String value) {
549 evaluator = (TriggeringEventEvaluator)
550 OptionConverter.instantiateByClassName(value,
551 TriggeringEventEvaluator.class,
557 The <b>LocationInfo</b> option takes a boolean value. By
558 default, it is set to false which means there will be no effort
559 to extract the location information related to the event. As a
560 result, the layout that formats the events as they are sent out
561 in an e-mail is likely to place the wrong location information
562 (if present in the format).
564 <p>Location information extraction is comparatively very slow and
565 should be avoided unless performance is not a concern.
568 void setLocationInfo(boolean locationInfo) {
569 this.locationInfo = locationInfo;
573 Returns value of the <b>LocationInfo</b> option.
576 boolean getLocationInfo() {
581 Set the cc recipient addresses.
582 @param addresses recipient addresses as comma separated string, may be null.
585 public void setCc(final String addresses) {
590 Get the cc recipient addresses.
591 @return recipient addresses as comma separated string, may be null.
594 public String getCc() {
599 Set the bcc recipient addresses.
600 @param addresses recipient addresses as comma separated string, may be null.
603 public void setBcc(final String addresses) {
604 this.bcc = addresses;
608 Get the bcc recipient addresses.
609 @return recipient addresses as comma separated string, may be null.
612 public String getBcc() {
617 * The <b>SmtpPassword</b> option takes a string value which should be the password required to authenticate against
619 * @param password password, may be null.
622 public void setSMTPPassword(final String password) {
623 this.smtpPassword = password;
627 * The <b>SmtpUsername</b> option takes a string value which should be the username required to authenticate against
629 * @param username user name, may be null.
632 public void setSMTPUsername(final String username) {
633 this.smtpUsername = username;
637 * Setting the <b>SmtpDebug</b> option to true will cause the mail session to log its server interaction to stdout.
638 * This can be useful when debuging the appender but should not be used during production because username and
639 * password information is included in the output.
640 * @param debug debug flag.
643 public void setSMTPDebug(final boolean debug) {
644 this.smtpDebug = debug;
649 * @return SMTP password, may be null.
652 public String getSMTPPassword() {
657 * Get SMTP user name.
658 * @return SMTP user name, may be null.
661 public String getSMTPUsername() {
667 * @return SMTP debug flag.
670 public boolean getSMTPDebug() {
675 * Sets triggering evaluator.
676 * @param trigger triggering event evaluator.
679 public final void setEvaluator(final TriggeringEventEvaluator trigger) {
680 if (trigger == null) {
681 throw new NullPointerException("trigger");
683 this.evaluator = trigger;
687 * Get triggering evaluator.
688 * @return triggering event evaluator.
691 public final TriggeringEventEvaluator getEvaluator() {
698 public boolean parseUnrecognizedElement(final Element element,
699 final Properties props) throws Exception {
700 if ("triggeringPolicy".equals(element.getNodeName())) {
701 Object triggerPolicy =
702 org.apache.log4j.xml.DOMConfigurator.parseElement(
703 element, props, TriggeringEventEvaluator.class);
704 if (triggerPolicy instanceof TriggeringEventEvaluator) {
705 setEvaluator((TriggeringEventEvaluator) triggerPolicy);
714 * Get transport protocol.
715 * Typically null or "smtps".
717 * @return transport protocol, may be null.
720 public final String getSMTPProtocol() {
725 * Set transport protocol.
726 * Typically null or "smtps".
728 * @param val transport protocol, may be null.
731 public final void setSMTPProtocol(final String val) {
738 * @return port, negative values indicate use of default ports for protocol.
741 public final int getSMTPPort() {
748 * @param val port, negative values indicate use of default ports for protocol.
751 public final void setSMTPPort(final int val) {
758 * @return if true all buffered logging events will be sent when the appender is closed.
761 public final boolean getSendOnClose() {
768 * @param val if true all buffered logging events will be sent when appender is closed.
771 public final void setSendOnClose(final boolean val) {
777 class DefaultEvaluator implements TriggeringEventEvaluator {
779 Is this <code>event</code> the e-mail triggering event?
781 <p>This method returns <code>true</code>, if the event level
782 has ERROR level or higher. Otherwise it returns
783 <code>false</code>. */
785 boolean isTriggeringEvent(LoggingEvent event) {
786 return event.getLevel().isGreaterOrEqual(Level.ERROR);