1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 package org.apache.log4j.net;
19
20 import org.apache.log4j.AppenderSkeleton;
21 import org.apache.log4j.Level;
22 import org.apache.log4j.Layout;
23 import org.apache.log4j.xml.UnrecognizedElementHandler;
24 import org.apache.log4j.helpers.CyclicBuffer;
25 import org.apache.log4j.helpers.OptionConverter;
26 import org.apache.log4j.helpers.LogLog;
27 import org.apache.log4j.spi.LoggingEvent;
28 import org.apache.log4j.spi.ErrorCode;
29 import org.apache.log4j.spi.TriggeringEventEvaluator;
30 import org.apache.log4j.spi.OptionHandler;
31 import org.w3c.dom.Element;
32
33 import java.util.Properties;
34 import java.util.Date;
35
36 import javax.mail.Session;
37 import javax.mail.Authenticator;
38 import javax.mail.PasswordAuthentication;
39 import javax.mail.Transport;
40 import javax.mail.Message;
41 import javax.mail.MessagingException;
42 import javax.mail.internet.MimeMessage;
43 import javax.mail.Multipart;
44 import javax.mail.internet.MimeMultipart;
45 import javax.mail.internet.MimeBodyPart;
46 import javax.mail.internet.InternetAddress;
47 import javax.mail.internet.AddressException;
48
49 /***
50 Send an e-mail when a specific logging event occurs, typically on
51 errors or fatal errors.
52
53 <p>The number of logging events delivered in this e-mail depend on
54 the value of <b>BufferSize</b> option. The
55 <code>SMTPAppender</code> keeps only the last
56 <code>BufferSize</code> logging events in its cyclic buffer. This
57 keeps memory requirements at a reasonable level while still
58 delivering useful application context.
59
60 By default, an email message will be sent when an ERROR or higher
61 severity message is appended. The triggering criteria can be
62 modified by setting the evaluatorClass property with the name
63 of a class implementing TriggeringEventEvaluator, setting the evaluator
64 property with an instance of TriggeringEventEvaluator or
65 nesting a triggeringPolicy element where the specified
66 class implements TriggeringEventEvaluator.
67
68 @author Ceki Gülcü
69 @since 1.0 */
70 public class SMTPAppender extends AppenderSkeleton
71 implements UnrecognizedElementHandler {
72 private String to;
73 /***
74 * Comma separated list of cc recipients.
75 */
76 private String cc;
77 /***
78 * Comma separated list of bcc recipients.
79 */
80 private String bcc;
81 private String from;
82 private String subject;
83 private String smtpHost;
84 private String smtpUsername;
85 private String smtpPassword;
86 private boolean smtpDebug = false;
87 private int bufferSize = 512;
88 private boolean locationInfo = false;
89
90 protected CyclicBuffer cb = new CyclicBuffer(bufferSize);
91 protected Message msg;
92
93 protected TriggeringEventEvaluator evaluator;
94
95
96
97 /***
98 The default constructor will instantiate the appender with a
99 {@link TriggeringEventEvaluator} that will trigger on events with
100 level ERROR or higher.*/
101 public
102 SMTPAppender() {
103 this(new DefaultEvaluator());
104 }
105
106
107 /***
108 Use <code>evaluator</code> passed as parameter as the {@link
109 TriggeringEventEvaluator} for this SMTPAppender. */
110 public
111 SMTPAppender(TriggeringEventEvaluator evaluator) {
112 this.evaluator = evaluator;
113 }
114
115
116 /***
117 Activate the specified options, such as the smtp host, the
118 recipient, from, etc. */
119 public
120 void activateOptions() {
121 Session session = createSession();
122 msg = new MimeMessage(session);
123
124 try {
125 addressMessage(msg);
126 if(subject != null) {
127 msg.setSubject(subject);
128 }
129 } catch(MessagingException e) {
130 LogLog.error("Could not activate SMTPAppender options.", e );
131 }
132
133 if (evaluator instanceof OptionHandler) {
134 ((OptionHandler) evaluator).activateOptions();
135 }
136 }
137
138 /***
139 * Address message.
140 * @param msg message, may not be null.
141 * @throws MessagingException thrown if error addressing message.
142 */
143 protected void addressMessage(final Message msg) throws MessagingException {
144 if (from != null) {
145 msg.setFrom(getAddress(from));
146 } else {
147 msg.setFrom();
148 }
149
150 if (to != null && to.length() > 0) {
151 msg.setRecipients(Message.RecipientType.TO, parseAddress(to));
152 }
153
154
155 if (cc != null && cc.length() > 0) {
156 msg.setRecipients(Message.RecipientType.CC, parseAddress(cc));
157 }
158
159
160 if (bcc != null && bcc.length() > 0) {
161 msg.setRecipients(Message.RecipientType.BCC, parseAddress(bcc));
162 }
163 }
164
165 /***
166 * Create mail session.
167 * @return mail session, may not be null.
168 */
169 protected Session createSession() {
170 Properties props = null;
171 try {
172 props = new Properties (System.getProperties());
173 } catch(SecurityException ex) {
174 props = new Properties();
175 }
176 if (smtpHost != null) {
177 props.put("mail.smtp.host", smtpHost);
178 }
179
180 Authenticator auth = null;
181 if(smtpPassword != null && smtpUsername != null) {
182 props.put("mail.smtp.auth", "true");
183 auth = new Authenticator() {
184 protected PasswordAuthentication getPasswordAuthentication() {
185 return new PasswordAuthentication(smtpUsername, smtpPassword);
186 }
187 };
188 }
189 Session session = Session.getInstance(props, auth);
190 if (smtpDebug) {
191 session.setDebug(smtpDebug);
192 }
193 return session;
194 }
195
196 /***
197 Perform SMTPAppender specific appending actions, mainly adding
198 the event to a cyclic buffer and checking if the event triggers
199 an e-mail to be sent. */
200 public
201 void append(LoggingEvent event) {
202
203 if(!checkEntryConditions()) {
204 return;
205 }
206
207 event.getThreadName();
208 event.getNDC();
209 event.getMDCCopy();
210 if(locationInfo) {
211 event.getLocationInformation();
212 }
213 cb.add(event);
214 if(evaluator.isTriggeringEvent(event)) {
215 sendBuffer();
216 }
217 }
218
219 /***
220 This method determines if there is a sense in attempting to append.
221
222 <p>It checks whether there is a set output target and also if
223 there is a set layout. If these checks fail, then the boolean
224 value <code>false</code> is returned. */
225 protected
226 boolean checkEntryConditions() {
227 if(this.msg == null) {
228 errorHandler.error("Message object not configured.");
229 return false;
230 }
231
232 if(this.evaluator == null) {
233 errorHandler.error("No TriggeringEventEvaluator is set for appender ["+
234 name+"].");
235 return false;
236 }
237
238
239 if(this.layout == null) {
240 errorHandler.error("No layout set for appender named ["+name+"].");
241 return false;
242 }
243 return true;
244 }
245
246
247 synchronized
248 public
249 void close() {
250 this.closed = true;
251 }
252
253 InternetAddress getAddress(String addressStr) {
254 try {
255 return new InternetAddress(addressStr);
256 } catch(AddressException e) {
257 errorHandler.error("Could not parse address ["+addressStr+"].", e,
258 ErrorCode.ADDRESS_PARSE_FAILURE);
259 return null;
260 }
261 }
262
263 InternetAddress[] parseAddress(String addressStr) {
264 try {
265 return InternetAddress.parse(addressStr, true);
266 } catch(AddressException e) {
267 errorHandler.error("Could not parse address ["+addressStr+"].", e,
268 ErrorCode.ADDRESS_PARSE_FAILURE);
269 return null;
270 }
271 }
272
273 /***
274 Returns value of the <b>To</b> option.
275 */
276 public
277 String getTo() {
278 return to;
279 }
280
281
282 /***
283 The <code>SMTPAppender</code> requires a {@link
284 org.apache.log4j.Layout layout}. */
285 public
286 boolean requiresLayout() {
287 return true;
288 }
289
290 /***
291 Send the contents of the cyclic buffer as an e-mail message.
292 */
293 protected
294 void sendBuffer() {
295
296
297
298 try {
299 MimeBodyPart part = new MimeBodyPart();
300
301 StringBuffer sbuf = new StringBuffer();
302 String t = layout.getHeader();
303 if(t != null)
304 sbuf.append(t);
305 int len = cb.length();
306 for(int i = 0; i < len; i++) {
307
308 LoggingEvent event = cb.get();
309 sbuf.append(layout.format(event));
310 if(layout.ignoresThrowable()) {
311 String[] s = event.getThrowableStrRep();
312 if (s != null) {
313 for(int j = 0; j < s.length; j++) {
314 sbuf.append(s[j]);
315 sbuf.append(Layout.LINE_SEP);
316 }
317 }
318 }
319 }
320 t = layout.getFooter();
321 if(t != null)
322 sbuf.append(t);
323 part.setContent(sbuf.toString(), layout.getContentType());
324
325 Multipart mp = new MimeMultipart();
326 mp.addBodyPart(part);
327 msg.setContent(mp);
328
329 msg.setSentDate(new Date());
330 Transport.send(msg);
331 } catch(Exception e) {
332 LogLog.error("Error occured while sending e-mail notification.", e);
333 }
334 }
335
336
337
338 /***
339 Returns value of the <b>EvaluatorClass</b> option.
340 */
341 public
342 String getEvaluatorClass() {
343 return evaluator == null ? null : evaluator.getClass().getName();
344 }
345
346 /***
347 Returns value of the <b>From</b> option.
348 */
349 public
350 String getFrom() {
351 return from;
352 }
353
354 /***
355 Returns value of the <b>Subject</b> option.
356 */
357 public
358 String getSubject() {
359 return subject;
360 }
361
362 /***
363 The <b>From</b> option takes a string value which should be a
364 e-mail address of the sender.
365 */
366 public
367 void setFrom(String from) {
368 this.from = from;
369 }
370
371 /***
372 The <b>Subject</b> option takes a string value which should be a
373 the subject of the e-mail message.
374 */
375 public
376 void setSubject(String subject) {
377 this.subject = subject;
378 }
379
380
381 /***
382 The <b>BufferSize</b> option takes a positive integer
383 representing the maximum number of logging events to collect in a
384 cyclic buffer. When the <code>BufferSize</code> is reached,
385 oldest events are deleted as new events are added to the
386 buffer. By default the size of the cyclic buffer is 512 events.
387 */
388 public
389 void setBufferSize(int bufferSize) {
390 this.bufferSize = bufferSize;
391 cb.resize(bufferSize);
392 }
393
394 /***
395 The <b>SMTPHost</b> option takes a string value which should be a
396 the host name of the SMTP server that will send the e-mail message.
397 */
398 public
399 void setSMTPHost(String smtpHost) {
400 this.smtpHost = smtpHost;
401 }
402
403 /***
404 Returns value of the <b>SMTPHost</b> option.
405 */
406 public
407 String getSMTPHost() {
408 return smtpHost;
409 }
410
411 /***
412 The <b>To</b> option takes a string value which should be a
413 comma separated list of e-mail address of the recipients.
414 */
415 public
416 void setTo(String to) {
417 this.to = to;
418 }
419
420
421
422 /***
423 Returns value of the <b>BufferSize</b> option.
424 */
425 public
426 int getBufferSize() {
427 return bufferSize;
428 }
429
430 /***
431 The <b>EvaluatorClass</b> option takes a string value
432 representing the name of the class implementing the {@link
433 TriggeringEventEvaluator} interface. A corresponding object will
434 be instantiated and assigned as the triggering event evaluator
435 for the SMTPAppender.
436 */
437 public
438 void setEvaluatorClass(String value) {
439 evaluator = (TriggeringEventEvaluator)
440 OptionConverter.instantiateByClassName(value,
441 TriggeringEventEvaluator.class,
442 evaluator);
443 }
444
445
446 /***
447 The <b>LocationInfo</b> option takes a boolean value. By
448 default, it is set to false which means there will be no effort
449 to extract the location information related to the event. As a
450 result, the layout that formats the events as they are sent out
451 in an e-mail is likely to place the wrong location information
452 (if present in the format).
453
454 <p>Location information extraction is comparatively very slow and
455 should be avoided unless performance is not a concern.
456 */
457 public
458 void setLocationInfo(boolean locationInfo) {
459 this.locationInfo = locationInfo;
460 }
461
462 /***
463 Returns value of the <b>LocationInfo</b> option.
464 */
465 public
466 boolean getLocationInfo() {
467 return locationInfo;
468 }
469
470 /***
471 Set the cc recipient addresses.
472 @param addresses recipient addresses as comma separated string, may be null.
473 */
474 public void setCc(final String addresses) {
475 this.cc = addresses;
476 }
477
478 /***
479 Get the cc recipient addresses.
480 @return recipient addresses as comma separated string, may be null.
481 */
482 public String getCc() {
483 return cc;
484 }
485
486 /***
487 Set the bcc recipient addresses.
488 @param addresses recipient addresses as comma separated string, may be null.
489 */
490 public void setBcc(final String addresses) {
491 this.bcc = addresses;
492 }
493
494 /***
495 Get the bcc recipient addresses.
496 @return recipient addresses as comma separated string, may be null.
497 */
498 public String getBcc() {
499 return bcc;
500 }
501
502 /***
503 * The <b>SmtpPassword</b> option takes a string value which should be the password required to authenticate against
504 * the mail server.
505 * @param password password, may be null.
506 */
507 public void setSMTPPassword(final String password) {
508 this.smtpPassword = password;
509 }
510
511 /***
512 * The <b>SmtpUsername</b> option takes a string value which should be the username required to authenticate against
513 * the mail server.
514 * @param username user name, may be null.
515 */
516 public void setSMTPUsername(final String username) {
517 this.smtpUsername = username;
518 }
519
520 /***
521 * Setting the <b>SmtpDebug</b> option to true will cause the mail session to log its server interaction to stdout.
522 * This can be useful when debuging the appender but should not be used during production because username and
523 * password information is included in the output.
524 * @param debug debug flag.
525 */
526 public void setSMTPDebug(final boolean debug) {
527 this.smtpDebug = debug;
528 }
529
530 /***
531 * Get SMTP password.
532 * @return SMTP password, may be null.
533 */
534 public String getSMTPPassword() {
535 return smtpPassword;
536 }
537
538 /***
539 * Get SMTP user name.
540 * @return SMTP user name, may be null.
541 */
542 public String getSMTPUsername() {
543 return smtpUsername;
544 }
545
546 /***
547 * Get SMTP debug.
548 * @return SMTP debug flag.
549 */
550 public boolean getSMTPDebug() {
551 return smtpDebug;
552 }
553
554 /***
555 * Sets triggering evaluator.
556 * @param trigger triggering event evaluator.
557 * @since 1.2.15
558 */
559 public final void setEvaluator(final TriggeringEventEvaluator trigger) {
560 if (trigger == null) {
561 throw new NullPointerException("trigger");
562 }
563 this.evaluator = trigger;
564 }
565
566 /***
567 * Get triggering evaluator.
568 * @return triggering event evaluator.
569 * @since 1.2.15
570 */
571 public final TriggeringEventEvaluator getEvaluator() {
572 return evaluator;
573 }
574
575 /*** {@inheritDoc} */
576 public boolean parseUnrecognizedElement(final Element element,
577 final Properties props) throws Exception {
578 if ("triggeringPolicy".equals(element.getNodeName())) {
579 Object triggerPolicy =
580 org.apache.log4j.xml.DOMConfigurator.parseElement(
581 element, props, TriggeringEventEvaluator.class);
582 if (triggerPolicy instanceof TriggeringEventEvaluator) {
583 setEvaluator((TriggeringEventEvaluator) triggerPolicy);
584 }
585 return true;
586 }
587
588 return false;
589 }
590
591 }
592
593 class DefaultEvaluator implements TriggeringEventEvaluator {
594 /***
595 Is this <code>event</code> the e-mail triggering event?
596
597 <p>This method returns <code>true</code>, if the event level
598 has ERROR level or higher. Otherwise it returns
599 <code>false</code>. */
600 public
601 boolean isTriggeringEvent(LoggingEvent event) {
602 return event.getLevel().isGreaterOrEqual(Level.ERROR);
603 }
604 }