001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018package org.apache.log4j.net; 019 020import org.apache.log4j.AppenderSkeleton; 021import org.apache.log4j.Layout; 022import org.apache.log4j.Level; 023import org.apache.log4j.helpers.CyclicBuffer; 024import org.apache.log4j.helpers.LogLog; 025import org.apache.log4j.helpers.OptionConverter; 026import org.apache.log4j.spi.ErrorCode; 027import org.apache.log4j.spi.LoggingEvent; 028import org.apache.log4j.spi.OptionHandler; 029import org.apache.log4j.spi.TriggeringEventEvaluator; 030import org.apache.log4j.xml.UnrecognizedElementHandler; 031import org.w3c.dom.Element; 032 033import javax.mail.Authenticator; 034import javax.mail.Message; 035import javax.mail.MessagingException; 036import javax.mail.Multipart; 037import javax.mail.PasswordAuthentication; 038import javax.mail.Session; 039import javax.mail.Transport; 040import javax.mail.internet.AddressException; 041import javax.mail.internet.InternetAddress; 042import javax.mail.internet.InternetHeaders; 043import javax.mail.internet.MimeBodyPart; 044import javax.mail.internet.MimeMessage; 045import javax.mail.internet.MimeMultipart; 046import javax.mail.internet.MimeUtility; 047import java.io.ByteArrayOutputStream; 048import java.io.OutputStreamWriter; 049import java.io.UnsupportedEncodingException; 050import java.io.Writer; 051import java.util.Date; 052import java.util.Properties; 053 054/** 055 Send an e-mail when a specific logging event occurs, typically on 056 errors or fatal errors. 057 058 <p>The number of logging events delivered in this e-mail depend on 059 the value of <b>BufferSize</b> option. The 060 <code>SMTPAppender</code> keeps only the last 061 <code>BufferSize</code> logging events in its cyclic buffer. This 062 keeps memory requirements at a reasonable level while still 063 delivering useful application context. 064 065 By default, an email message will be sent when an ERROR or higher 066 severity message is appended. The triggering criteria can be 067 modified by setting the evaluatorClass property with the name 068 of a class implementing TriggeringEventEvaluator, setting the evaluator 069 property with an instance of TriggeringEventEvaluator or 070 nesting a triggeringPolicy element where the specified 071 class implements TriggeringEventEvaluator. 072 073 This class has implemented UnrecognizedElementHandler since 1.2.15. 074 075 Since 1.2.16, SMTP over SSL is supported by setting SMTPProtocol to "smpts". 076 077 @author Ceki Gülcü 078 @since 1.0 */ 079public class SMTPAppender extends AppenderSkeleton 080 implements UnrecognizedElementHandler { 081 private String to; 082 /** 083 * Comma separated list of cc recipients. 084 */ 085 private String cc; 086 /** 087 * Comma separated list of bcc recipients. 088 */ 089 private String bcc; 090 private String from; 091 /** 092 * Comma separated list of replyTo addresses. 093 */ 094 private String replyTo; 095 private String subject; 096 private String smtpHost; 097 private String smtpUsername; 098 private String smtpPassword; 099 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; 105 106 protected CyclicBuffer cb = new CyclicBuffer(bufferSize); 107 protected Message msg; 108 109 protected TriggeringEventEvaluator evaluator; 110 111 112 113 /** 114 The default constructor will instantiate the appender with a 115 {@link TriggeringEventEvaluator} that will trigger on events with 116 level ERROR or higher.*/ 117 public 118 SMTPAppender() { 119 this(new DefaultEvaluator()); 120 } 121 122 123 /** 124 Use <code>evaluator</code> passed as parameter as the {@link 125 TriggeringEventEvaluator} for this SMTPAppender. */ 126 public 127 SMTPAppender(TriggeringEventEvaluator evaluator) { 128 this.evaluator = evaluator; 129 } 130 131 132 /** 133 Activate the specified options, such as the smtp host, the 134 recipient, from, etc. */ 135 public 136 void activateOptions() { 137 Session session = createSession(); 138 msg = new MimeMessage(session); 139 140 try { 141 addressMessage(msg); 142 if(subject != null) { 143 try { 144 msg.setSubject(MimeUtility.encodeText(subject, "UTF-8", null)); 145 } catch(UnsupportedEncodingException ex) { 146 LogLog.error("Unable to encode SMTP subject", ex); 147 } 148 } 149 } catch(MessagingException e) { 150 LogLog.error("Could not activate SMTPAppender options.", e ); 151 } 152 153 if (evaluator instanceof OptionHandler) { 154 ((OptionHandler) evaluator).activateOptions(); 155 } 156 } 157 158 /** 159 * Address message. 160 * @param msg message, may not be null. 161 * @throws MessagingException thrown if error addressing message. 162 * @since 1.2.14 163 */ 164 protected void addressMessage(final Message msg) throws MessagingException { 165 if (from != null) { 166 msg.setFrom(getAddress(from)); 167 } else { 168 msg.setFrom(); 169 } 170 171 //Add ReplyTo addresses if defined. 172 if (replyTo != null && replyTo.length() > 0) { 173 msg.setReplyTo(parseAddress(replyTo)); 174 } 175 176 if (to != null && to.length() > 0) { 177 msg.setRecipients(Message.RecipientType.TO, parseAddress(to)); 178 } 179 180 //Add CC receipients if defined. 181 if (cc != null && cc.length() > 0) { 182 msg.setRecipients(Message.RecipientType.CC, parseAddress(cc)); 183 } 184 185 //Add BCC receipients if defined. 186 if (bcc != null && bcc.length() > 0) { 187 msg.setRecipients(Message.RecipientType.BCC, parseAddress(bcc)); 188 } 189 } 190 191 /** 192 * Create mail session. 193 * @return mail session, may not be null. 194 * @since 1.2.14 195 */ 196 protected Session createSession() { 197 Properties props = null; 198 try { 199 props = new Properties (System.getProperties()); 200 } catch(SecurityException ex) { 201 props = new Properties(); 202 } 203 204 String prefix = "mail.smtp"; 205 if (smtpProtocol != null) { 206 props.put("mail.transport.protocol", smtpProtocol); 207 prefix = "mail." + smtpProtocol; 208 } 209 if (smtpHost != null) { 210 props.put(prefix + ".host", smtpHost); 211 } 212 if (smtpPort > 0) { 213 props.put(prefix + ".port", String.valueOf(smtpPort)); 214 } 215 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); 222 } 223 }; 224 } 225 Session session = Session.getInstance(props, auth); 226 if (smtpProtocol != null) { 227 session.setProtocolForAddress("rfc822", smtpProtocol); 228 } 229 if (smtpDebug) { 230 session.setDebug(smtpDebug); 231 } 232 return session; 233 } 234 235 /** 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. */ 239 public 240 void append(LoggingEvent event) { 241 242 if(!checkEntryConditions()) { 243 return; 244 } 245 246 event.getThreadName(); 247 event.getNDC(); 248 event.getMDCCopy(); 249 if(locationInfo) { 250 event.getLocationInformation(); 251 } 252 event.getRenderedMessage(); 253 event.getThrowableStrRep(); 254 cb.add(event); 255 if(evaluator.isTriggeringEvent(event)) { 256 sendBuffer(); 257 } 258 } 259 260 /** 261 This method determines if there is a sense in attempting to append. 262 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. */ 266 protected 267 boolean checkEntryConditions() { 268 if(this.msg == null) { 269 errorHandler.error("Message object not configured."); 270 return false; 271 } 272 273 if(this.evaluator == null) { 274 errorHandler.error("No TriggeringEventEvaluator is set for appender ["+ 275 name+"]."); 276 return false; 277 } 278 279 280 if(this.layout == null) { 281 errorHandler.error("No layout set for appender named ["+name+"]."); 282 return false; 283 } 284 return true; 285 } 286 287 288 synchronized 289 public 290 void close() { 291 this.closed = true; 292 if (sendOnClose && cb.length() > 0) { 293 sendBuffer(); 294 } 295 } 296 297 InternetAddress getAddress(String addressStr) { 298 try { 299 return new InternetAddress(addressStr); 300 } catch(AddressException e) { 301 errorHandler.error("Could not parse address ["+addressStr+"].", e, 302 ErrorCode.ADDRESS_PARSE_FAILURE); 303 return null; 304 } 305 } 306 307 InternetAddress[] parseAddress(String addressStr) { 308 try { 309 return InternetAddress.parse(addressStr, true); 310 } catch(AddressException e) { 311 errorHandler.error("Could not parse address ["+addressStr+"].", e, 312 ErrorCode.ADDRESS_PARSE_FAILURE); 313 return null; 314 } 315 } 316 317 /** 318 Returns value of the <b>To</b> option. 319 */ 320 public 321 String getTo() { 322 return to; 323 } 324 325 326 /** 327 The <code>SMTPAppender</code> requires a {@link 328 org.apache.log4j.Layout layout}. */ 329 public 330 boolean requiresLayout() { 331 return true; 332 } 333 334 /** 335 * Layout body of email message. 336 * @since 1.2.16 337 */ 338 protected String formatBody() { 339 340 // Note: this code already owns the monitor for this 341 // appender. This frees us from needing to synchronize on 'cb'. 342 343 StringBuffer sbuf = new StringBuffer(); 344 String t = layout.getHeader(); 345 if(t != null) 346 sbuf.append(t); 347 int len = cb.length(); 348 for(int i = 0; i < len; i++) { 349 //sbuf.append(MimeUtility.encodeText(layout.format(cb.get()))); 350 LoggingEvent event = cb.get(); 351 sbuf.append(layout.format(event)); 352 if(layout.ignoresThrowable()) { 353 String[] s = event.getThrowableStrRep(); 354 if (s != null) { 355 for(int j = 0; j < s.length; j++) { 356 sbuf.append(s[j]); 357 sbuf.append(Layout.LINE_SEP); 358 } 359 } 360 } 361 } 362 t = layout.getFooter(); 363 if(t != null) { 364 sbuf.append(t); 365 } 366 367 return sbuf.toString(); 368 } 369 370 /** 371 Send the contents of the cyclic buffer as an e-mail message. 372 */ 373 protected 374 void sendBuffer() { 375 376 try { 377 String s = formatBody(); 378 boolean allAscii = true; 379 for(int i = 0; i < s.length() && allAscii; i++) { 380 allAscii = s.charAt(i) <= 0x7F; 381 } 382 MimeBodyPart part; 383 if (allAscii) { 384 part = new MimeBodyPart(); 385 part.setContent(s, layout.getContentType()); 386 } else { 387 try { 388 ByteArrayOutputStream os = new ByteArrayOutputStream(); 389 Writer writer = new OutputStreamWriter( 390 MimeUtility.encode(os, "quoted-printable"), "UTF-8"); 391 writer.write(s); 392 writer.close(); 393 InternetHeaders headers = new InternetHeaders(); 394 headers.setHeader("Content-Type", layout.getContentType() + "; charset=UTF-8"); 395 headers.setHeader("Content-Transfer-Encoding", "quoted-printable"); 396 part = new MimeBodyPart(headers, os.toByteArray()); 397 } catch(Exception ex) { 398 StringBuffer sbuf = new StringBuffer(s); 399 for (int i = 0; i < sbuf.length(); i++) { 400 if (sbuf.charAt(i) >= 0x80) { 401 sbuf.setCharAt(i, '?'); 402 } 403 } 404 part = new MimeBodyPart(); 405 part.setContent(sbuf.toString(), layout.getContentType()); 406 } 407 } 408 409 410 411 Multipart mp = new MimeMultipart(); 412 mp.addBodyPart(part); 413 msg.setContent(mp); 414 415 msg.setSentDate(new Date()); 416 Transport.send(msg); 417 } catch(MessagingException e) { 418 LogLog.error("Error occured while sending e-mail notification.", e); 419 } catch(RuntimeException e) { 420 LogLog.error("Error occured while sending e-mail notification.", e); 421 } 422 } 423 424 425 426 /** 427 Returns value of the <b>EvaluatorClass</b> option. 428 */ 429 public 430 String getEvaluatorClass() { 431 return evaluator == null ? null : evaluator.getClass().getName(); 432 } 433 434 /** 435 Returns value of the <b>From</b> option. 436 */ 437 public 438 String getFrom() { 439 return from; 440 } 441 442 /** 443 Get the reply addresses. 444 @return reply addresses as comma separated string, may be null. 445 @since 1.2.16 446 */ 447 public 448 String getReplyTo() { 449 return replyTo; 450 } 451 452 /** 453 Returns value of the <b>Subject</b> option. 454 */ 455 public 456 String getSubject() { 457 return subject; 458 } 459 460 /** 461 The <b>From</b> option takes a string value which should be a 462 e-mail address of the sender. 463 */ 464 public 465 void setFrom(String from) { 466 this.from = from; 467 } 468 469 /** 470 Set the e-mail addresses to which replies should be directed. 471 @param addresses reply addresses as comma separated string, may be null. 472 @since 1.2.16 473 */ 474 public 475 void setReplyTo(final String addresses) { 476 this.replyTo = addresses; 477 } 478 479 480 /** 481 The <b>Subject</b> option takes a string value which should be a 482 the subject of the e-mail message. 483 */ 484 public 485 void setSubject(String subject) { 486 this.subject = subject; 487 } 488 489 490 /** 491 The <b>BufferSize</b> option takes a positive integer 492 representing the maximum number of logging events to collect in a 493 cyclic buffer. When the <code>BufferSize</code> is reached, 494 oldest events are deleted as new events are added to the 495 buffer. By default the size of the cyclic buffer is 512 events. 496 */ 497 public 498 void setBufferSize(int bufferSize) { 499 this.bufferSize = bufferSize; 500 cb.resize(bufferSize); 501 } 502 503 /** 504 The <b>SMTPHost</b> option takes a string value which should be a 505 the host name of the SMTP server that will send the e-mail message. 506 */ 507 public 508 void setSMTPHost(String smtpHost) { 509 this.smtpHost = smtpHost; 510 } 511 512 /** 513 Returns value of the <b>SMTPHost</b> option. 514 */ 515 public 516 String getSMTPHost() { 517 return smtpHost; 518 } 519 520 /** 521 The <b>To</b> option takes a string value which should be a 522 comma separated list of e-mail address of the recipients. 523 */ 524 public 525 void setTo(String to) { 526 this.to = to; 527 } 528 529 530 531 /** 532 Returns value of the <b>BufferSize</b> option. 533 */ 534 public 535 int getBufferSize() { 536 return bufferSize; 537 } 538 539 /** 540 The <b>EvaluatorClass</b> option takes a string value 541 representing the name of the class implementing the {@link 542 TriggeringEventEvaluator} interface. A corresponding object will 543 be instantiated and assigned as the triggering event evaluator 544 for the SMTPAppender. 545 */ 546 public 547 void setEvaluatorClass(String value) { 548 evaluator = (TriggeringEventEvaluator) 549 OptionConverter.instantiateByClassName(value, 550 TriggeringEventEvaluator.class, 551 evaluator); 552 } 553 554 555 /** 556 The <b>LocationInfo</b> option takes a boolean value. By 557 default, it is set to false which means there will be no effort 558 to extract the location information related to the event. As a 559 result, the layout that formats the events as they are sent out 560 in an e-mail is likely to place the wrong location information 561 (if present in the format). 562 563 <p>Location information extraction is comparatively very slow and 564 should be avoided unless performance is not a concern. 565 */ 566 public 567 void setLocationInfo(boolean locationInfo) { 568 this.locationInfo = locationInfo; 569 } 570 571 /** 572 Returns value of the <b>LocationInfo</b> option. 573 */ 574 public 575 boolean getLocationInfo() { 576 return locationInfo; 577 } 578 579 /** 580 Set the cc recipient addresses. 581 @param addresses recipient addresses as comma separated string, may be null. 582 @since 1.2.14 583 */ 584 public void setCc(final String addresses) { 585 this.cc = addresses; 586 } 587 588 /** 589 Get the cc recipient addresses. 590 @return recipient addresses as comma separated string, may be null. 591 @since 1.2.14 592 */ 593 public String getCc() { 594 return cc; 595 } 596 597 /** 598 Set the bcc recipient addresses. 599 @param addresses recipient addresses as comma separated string, may be null. 600 @since 1.2.14 601 */ 602 public void setBcc(final String addresses) { 603 this.bcc = addresses; 604 } 605 606 /** 607 Get the bcc recipient addresses. 608 @return recipient addresses as comma separated string, may be null. 609 @since 1.2.14 610 */ 611 public String getBcc() { 612 return bcc; 613 } 614 615 /** 616 * The <b>SmtpPassword</b> option takes a string value which should be the password required to authenticate against 617 * the mail server. 618 * @param password password, may be null. 619 * @since 1.2.14 620 */ 621 public void setSMTPPassword(final String password) { 622 this.smtpPassword = password; 623 } 624 625 /** 626 * The <b>SmtpUsername</b> option takes a string value which should be the username required to authenticate against 627 * the mail server. 628 * @param username user name, may be null. 629 * @since 1.2.14 630 */ 631 public void setSMTPUsername(final String username) { 632 this.smtpUsername = username; 633 } 634 635 /** 636 * Setting the <b>SmtpDebug</b> option to true will cause the mail session to log its server interaction to stdout. 637 * This can be useful when debuging the appender but should not be used during production because username and 638 * password information is included in the output. 639 * @param debug debug flag. 640 * @since 1.2.14 641 */ 642 public void setSMTPDebug(final boolean debug) { 643 this.smtpDebug = debug; 644 } 645 646 /** 647 * Get SMTP password. 648 * @return SMTP password, may be null. 649 * @since 1.2.14 650 */ 651 public String getSMTPPassword() { 652 return smtpPassword; 653 } 654 655 /** 656 * Get SMTP user name. 657 * @return SMTP user name, may be null. 658 * @since 1.2.14 659 */ 660 public String getSMTPUsername() { 661 return smtpUsername; 662 } 663 664 /** 665 * Get SMTP debug. 666 * @return SMTP debug flag. 667 * @since 1.2.14 668 */ 669 public boolean getSMTPDebug() { 670 return smtpDebug; 671 } 672 673 /** 674 * Sets triggering evaluator. 675 * @param trigger triggering event evaluator. 676 * @since 1.2.15 677 */ 678 public final void setEvaluator(final TriggeringEventEvaluator trigger) { 679 if (trigger == null) { 680 throw new NullPointerException("trigger"); 681 } 682 this.evaluator = trigger; 683 } 684 685 /** 686 * Get triggering evaluator. 687 * @return triggering event evaluator. 688 * @since 1.2.15 689 */ 690 public final TriggeringEventEvaluator getEvaluator() { 691 return evaluator; 692 } 693 694 /** {@inheritDoc} 695 * @since 1.2.15 696 */ 697 public boolean parseUnrecognizedElement(final Element element, 698 final Properties props) throws Exception { 699 if ("triggeringPolicy".equals(element.getNodeName())) { 700 Object triggerPolicy = 701 org.apache.log4j.xml.DOMConfigurator.parseElement( 702 element, props, TriggeringEventEvaluator.class); 703 if (triggerPolicy instanceof TriggeringEventEvaluator) { 704 setEvaluator((TriggeringEventEvaluator) triggerPolicy); 705 } 706 return true; 707 } 708 709 return false; 710 } 711 712 /** 713 * Get transport protocol. 714 * Typically null or "smtps". 715 * 716 * @return transport protocol, may be null. 717 * @since 1.2.16 718 */ 719 public final String getSMTPProtocol() { 720 return smtpProtocol; 721 } 722 723 /** 724 * Set transport protocol. 725 * Typically null or "smtps". 726 * 727 * @param val transport protocol, may be null. 728 * @since 1.2.16 729 */ 730 public final void setSMTPProtocol(final String val) { 731 smtpProtocol = val; 732 } 733 734 /** 735 * Get port. 736 * 737 * @return port, negative values indicate use of default ports for protocol. 738 * @since 1.2.16 739 */ 740 public final int getSMTPPort() { 741 return smtpPort; 742 } 743 744 /** 745 * Set port. 746 * 747 * @param val port, negative values indicate use of default ports for protocol. 748 * @since 1.2.16 749 */ 750 public final void setSMTPPort(final int val) { 751 smtpPort = val; 752 } 753 754 /** 755 * Get sendOnClose. 756 * 757 * @return if true all buffered logging events will be sent when the appender is closed. 758 * @since 1.2.16 759 */ 760 public final boolean getSendOnClose() { 761 return sendOnClose; 762 } 763 764 /** 765 * Set sendOnClose. 766 * 767 * @param val if true all buffered logging events will be sent when appender is closed. 768 * @since 1.2.16 769 */ 770 public final void setSendOnClose(final boolean val) { 771 sendOnClose = val; 772 } 773 774} 775 776class DefaultEvaluator implements TriggeringEventEvaluator { 777 /** 778 Is this <code>event</code> the e-mail triggering event? 779 780 <p>This method returns <code>true</code>, if the event level 781 has ERROR level or higher. Otherwise it returns 782 <code>false</code>. */ 783 public 784 boolean isTriggeringEvent(LoggingEvent event) { 785 return event.getLevel().isGreaterOrEqual(Level.ERROR); 786 } 787}