001/** 002 * Logback: the reliable, generic, fast and flexible logging framework. 003 * Copyright (C) 1999-2015, QOS.ch. All rights reserved. 004 * 005 * This program and the accompanying materials are dual-licensed under 006 * either the terms of the Eclipse Public License v1.0 as published by 007 * the Eclipse Foundation 008 * 009 * or (per the licensee's choosing) 010 * 011 * under the terms of the GNU Lesser General Public License version 2.1 012 * as published by the Free Software Foundation. 013 */ 014package ch.qos.logback.core.net; 015 016import java.util.ArrayList; 017import java.util.Arrays; 018import java.util.Date; 019import java.util.List; 020import java.util.Properties; 021import java.util.concurrent.Future; 022 023import jakarta.mail.Message; 024import jakarta.mail.Multipart; 025import jakarta.mail.Session; 026import jakarta.mail.Transport; 027import jakarta.mail.internet.AddressException; 028import jakarta.mail.internet.InternetAddress; 029import jakarta.mail.internet.MimeBodyPart; 030import jakarta.mail.internet.MimeMessage; 031import jakarta.mail.internet.MimeMultipart; 032import javax.naming.Context; 033 034import ch.qos.logback.core.AppenderBase; 035import ch.qos.logback.core.CoreConstants; 036import ch.qos.logback.core.Layout; 037import ch.qos.logback.core.boolex.EvaluationException; 038import ch.qos.logback.core.boolex.EventEvaluator; 039import ch.qos.logback.core.helpers.CyclicBuffer; 040import ch.qos.logback.core.pattern.PatternLayoutBase; 041import ch.qos.logback.core.sift.DefaultDiscriminator; 042import ch.qos.logback.core.sift.Discriminator; 043import ch.qos.logback.core.spi.CyclicBufferTracker; 044import ch.qos.logback.core.util.ContentTypeUtil; 045import ch.qos.logback.core.util.JNDIUtil; 046import ch.qos.logback.core.util.OptionHelper; 047 048// Contributors: 049// Andrey Rybin charset encoding support http://jira.qos.ch/browse/LBCORE-69 050 051/** 052 * An abstract class that provides support for sending events to an email 053 * address. 054 * <p> 055 * See http://logback.qos.ch/manual/appenders.html#SMTPAppender for further 056 * documentation. 057 * 058 * @author Ceki Gülcü 059 * @author Sébastien Pennec 060 */ 061public abstract class SMTPAppenderBase<E> extends AppenderBase<E> { 062 063 static InternetAddress[] EMPTY_IA_ARRAY = new InternetAddress[0]; 064 // ~ 14 days 065 static final long MAX_DELAY_BETWEEN_STATUS_MESSAGES = 1228800 * CoreConstants.MILLIS_IN_ONE_SECOND; 066 067 long lastTrackerStatusPrint = 0; 068 long delayBetweenStatusMessages = 300 * CoreConstants.MILLIS_IN_ONE_SECOND; 069 070 protected Layout<E> subjectLayout; 071 protected Layout<E> layout; 072 073 private List<PatternLayoutBase<E>> toPatternLayoutList = new ArrayList<PatternLayoutBase<E>>(); 074 private String from; 075 private String subjectStr = null; 076 private String smtpHost; 077 private int smtpPort = 25; 078 private boolean starttls = false; 079 private boolean ssl = false; 080 private boolean sessionViaJNDI = false; 081 private String jndiLocation = CoreConstants.JNDI_COMP_PREFIX + "/mail/Session"; 082 083 String username; 084 String password; 085 String localhost; 086 087 boolean asynchronousSending = true; 088 protected Future<?> asynchronousSendingFuture = null; 089 private String charsetEncoding = "UTF-8"; 090 091 protected Session session; 092 093 protected EventEvaluator<E> eventEvaluator; 094 095 protected Discriminator<E> discriminator = new DefaultDiscriminator<E>(); 096 protected CyclicBufferTracker<E> cbTracker; 097 098 private int errorCount = 0; 099 100 /** 101 * return a layout for the subject string as appropriate for the module. If the 102 * subjectStr parameter is null, then a default value for subjectStr should be 103 * used. 104 * 105 * @param subjectStr 106 * @return a layout as appropriate for the module 107 */ 108 abstract protected Layout<E> makeSubjectLayout(String subjectStr); 109 110 /** 111 * Start the appender 112 */ 113 public void start() { 114 115 if (cbTracker == null) { 116 cbTracker = new CyclicBufferTracker<E>(); 117 } 118 119 if (sessionViaJNDI) 120 session = lookupSessionInJNDI(); 121 else 122 session = buildSessionFromProperties(); 123 124 if (session == null) { 125 addError("Failed to obtain javax.mail.Session. Cannot start."); 126 return; 127 } 128 129 subjectLayout = makeSubjectLayout(subjectStr); 130 131 started = true; 132 } 133 134 private Session lookupSessionInJNDI() { 135 addInfo("Looking up javax.mail.Session at JNDI location [" + jndiLocation + "]"); 136 try { 137 Context initialContext = JNDIUtil.getInitialContext(); 138 Object obj = JNDIUtil.lookupObject(initialContext, jndiLocation); 139 return (Session) obj; 140 } catch (Exception e) { 141 addError("Failed to obtain javax.mail.Session from JNDI location [" + jndiLocation + "]", e); 142 return null; 143 } 144 } 145 146 private Session buildSessionFromProperties() { 147 Properties props = new Properties(OptionHelper.getSystemProperties()); 148 if (smtpHost != null) { 149 props.put("mail.smtp.host", smtpHost); 150 } 151 props.put("mail.smtp.port", Integer.toString(smtpPort)); 152 153 if (localhost != null) { 154 props.put("mail.smtp.localhost", localhost); 155 } 156 157 LoginAuthenticator loginAuthenticator = null; 158 159 if (!OptionHelper.isNullOrEmptyOrAllSpaces(username)) { 160 loginAuthenticator = new LoginAuthenticator(username, password); 161 props.put("mail.smtp.auth", "true"); 162 } 163 164 if (isSTARTTLS() && isSSL()) { 165 addError("Both SSL and StartTLS cannot be enabled simultaneously"); 166 } else { 167 if (isSTARTTLS()) { 168 // see also http://jira.qos.ch/browse/LOGBACK-193 169 props.put("mail.smtp.starttls.enable", "true"); 170 props.put("mail.transport.protocol", "true"); 171 } 172 if (isSSL()) { 173 props.put("mail.smtp.ssl.enable", "true"); 174 } 175 } 176 177 // props.put("mail.debug", "true"); 178 179 return Session.getInstance(props, loginAuthenticator); 180 } 181 182 /** 183 * Perform SMTPAppender specific appending actions, delegating some of them to a 184 * subclass and checking if the event triggers an e-mail to be sent. 185 */ 186 protected void append(E eventObject) { 187 188 if (!checkEntryConditions()) { 189 return; 190 } 191 192 String key = discriminator.getDiscriminatingValue(eventObject); 193 long now = System.currentTimeMillis(); 194 final CyclicBuffer<E> cb = cbTracker.getOrCreate(key, now); 195 subAppend(cb, eventObject); 196 197 try { 198 if (eventEvaluator.evaluate(eventObject)) { 199 // clone the CyclicBuffer before sending out asynchronously 200 CyclicBuffer<E> cbClone = new CyclicBuffer<E>(cb); 201 // see http://jira.qos.ch/browse/LBCLASSIC-221 202 cb.clear(); 203 204 if (asynchronousSending) { 205 // perform actual sending asynchronously 206 SenderRunnable senderRunnable = new SenderRunnable(cbClone, eventObject); 207 this.asynchronousSendingFuture = context.getExecutorService().submit(senderRunnable); 208 } else { 209 // synchronous sending 210 sendBuffer(cbClone, eventObject); 211 } 212 } 213 } catch (EvaluationException ex) { 214 errorCount++; 215 if (errorCount < CoreConstants.MAX_ERROR_COUNT) { 216 addError("SMTPAppender's EventEvaluator threw an Exception-", ex); 217 } 218 } 219 220 // immediately remove the buffer if asked by the user 221 if (eventMarksEndOfLife(eventObject)) { 222 cbTracker.endOfLife(key); 223 } 224 225 cbTracker.removeStaleComponents(now); 226 227 if (lastTrackerStatusPrint + delayBetweenStatusMessages < now) { 228 addInfo("SMTPAppender [" + name + "] is tracking [" + cbTracker.getComponentCount() + "] buffers"); 229 lastTrackerStatusPrint = now; 230 // quadruple 'delay' assuming less than max delay 231 if (delayBetweenStatusMessages < MAX_DELAY_BETWEEN_STATUS_MESSAGES) { 232 delayBetweenStatusMessages *= 4; 233 } 234 } 235 } 236 237 abstract protected boolean eventMarksEndOfLife(E eventObject); 238 239 abstract protected void subAppend(CyclicBuffer<E> cb, E eventObject); 240 241 /** 242 * This method determines if there is a sense in attempting to append. 243 * <p> 244 * It checks whether there is a set output target and also if there is a set 245 * layout. If these checks fail, then the boolean value <code>false</code> is 246 * returned. 247 */ 248 public boolean checkEntryConditions() { 249 if (!this.started) { 250 addError("Attempting to append to a non-started appender: " + this.getName()); 251 return false; 252 } 253 254 if (this.eventEvaluator == null) { 255 addError("No EventEvaluator is set for appender [" + name + "]."); 256 return false; 257 } 258 259 if (this.layout == null) { 260 addError("No layout set for appender named [" + name 261 + "]. For more information, please visit http://logback.qos.ch/codes.html#smtp_no_layout"); 262 return false; 263 } 264 return true; 265 } 266 267 synchronized public void stop() { 268 this.started = false; 269 } 270 271 InternetAddress getAddress(String addressStr) { 272 try { 273 return new InternetAddress(addressStr); 274 } catch (AddressException e) { 275 addError("Could not parse address [" + addressStr + "].", e); 276 return null; 277 } 278 } 279 280 private List<InternetAddress> parseAddress(E event) { 281 int len = toPatternLayoutList.size(); 282 283 List<InternetAddress> iaList = new ArrayList<InternetAddress>(); 284 285 for (int i = 0; i < len; i++) { 286 try { 287 PatternLayoutBase<E> emailPL = toPatternLayoutList.get(i); 288 String emailAdrr = emailPL.doLayout(event); 289 if (emailAdrr == null || emailAdrr.length() == 0) { 290 continue; 291 } 292 InternetAddress[] tmp = InternetAddress.parse(emailAdrr, true); 293 iaList.addAll(Arrays.asList(tmp)); 294 } catch (AddressException e) { 295 addError("Could not parse email address for [" + toPatternLayoutList.get(i) + "] for event [" + event 296 + "]", e); 297 return iaList; 298 } 299 } 300 301 return iaList; 302 } 303 304 /** 305 * Returns value of the <b>toList</b> option. 306 */ 307 public List<PatternLayoutBase<E>> getToList() { 308 return toPatternLayoutList; 309 } 310 311 /** 312 * Allows to extend classes to update mime message (e.g.: Add headers) 313 */ 314 protected void updateMimeMsg(MimeMessage mimeMsg, CyclicBuffer<E> cb, E lastEventObject) { 315 } 316 317 /** 318 * Send the contents of the cyclic buffer as an e-mail message. 319 */ 320 @SuppressWarnings("null") 321 protected void sendBuffer(CyclicBuffer<E> cb, E lastEventObject) { 322 323 // Note: this code already owns the monitor for this 324 // appender. This frees us from needing to synchronize on 'cb'. 325 try { 326 MimeBodyPart part = new MimeBodyPart(); 327 328 StringBuffer sbuf = new StringBuffer(); 329 330 String header = layout.getFileHeader(); 331 if (header != null) { 332 sbuf.append(header); 333 } 334 String presentationHeader = layout.getPresentationHeader(); 335 if (presentationHeader != null) { 336 sbuf.append(presentationHeader); 337 } 338 fillBuffer(cb, sbuf); 339 String presentationFooter = layout.getPresentationFooter(); 340 if (presentationFooter != null) { 341 sbuf.append(presentationFooter); 342 } 343 String footer = layout.getFileFooter(); 344 if (footer != null) { 345 sbuf.append(footer); 346 } 347 348 String subjectStr = "Undefined subject"; 349 if (subjectLayout != null) { 350 subjectStr = subjectLayout.doLayout(lastEventObject); 351 352 // The subject must not contain new-line characters, which cause 353 // an SMTP error (LOGBACK-865). Truncate the string at the first 354 // new-line character. 355 int newLinePos = (subjectStr != null) ? subjectStr.indexOf('\n') : -1; 356 if (newLinePos > -1) { 357 subjectStr = subjectStr.substring(0, newLinePos); 358 } 359 } 360 361 MimeMessage mimeMsg = new MimeMessage(session); 362 363 if (from != null) { 364 mimeMsg.setFrom(getAddress(from)); 365 } else { 366 mimeMsg.setFrom(); 367 } 368 369 mimeMsg.setSubject(subjectStr, charsetEncoding); 370 371 List<InternetAddress> destinationAddresses = parseAddress(lastEventObject); 372 if (destinationAddresses.isEmpty()) { 373 addInfo("Empty destination address. Aborting email transmission"); 374 return; 375 } 376 377 InternetAddress[] toAddressArray = destinationAddresses.toArray(EMPTY_IA_ARRAY); 378 mimeMsg.setRecipients(Message.RecipientType.TO, toAddressArray); 379 380 String contentType = layout.getContentType(); 381 382 if (ContentTypeUtil.isTextual(contentType)) { 383 part.setText(sbuf.toString(), charsetEncoding, ContentTypeUtil.getSubType(contentType)); 384 } else { 385 part.setContent(sbuf.toString(), layout.getContentType()); 386 } 387 388 Multipart mp = new MimeMultipart(); 389 mp.addBodyPart(part); 390 mimeMsg.setContent(mp); 391 392 // Added the feature to update mime message before sending the email 393 updateMimeMsg(mimeMsg, cb, lastEventObject); 394 395 mimeMsg.setSentDate(new Date()); 396 addInfo("About to send out SMTP message \"" + subjectStr + "\" to " + Arrays.toString(toAddressArray)); 397 Transport.send(mimeMsg); 398 } catch (Exception e) { 399 addError("Error occurred while sending e-mail notification.", e); 400 } 401 } 402 403 abstract protected void fillBuffer(CyclicBuffer<E> cb, StringBuffer sbuf); 404 405 /** 406 * Returns value of the <b>From</b> option. 407 */ 408 public String getFrom() { 409 return from; 410 } 411 412 /** 413 * Returns value of the <b>Subject</b> option. 414 */ 415 public String getSubject() { 416 return subjectStr; 417 } 418 419 /** 420 * The <b>From</b> option takes a string value which should be an e-mail address 421 * of the sender. 422 */ 423 public void setFrom(String from) { 424 this.from = from; 425 } 426 427 /** 428 * The <b>Subject</b> option takes a string value which should be the subject 429 * of the e-mail message. 430 */ 431 public void setSubject(String subject) { 432 this.subjectStr = subject; 433 } 434 435 /** 436 * Alias for smtpHost 437 * 438 * @param smtpHost 439 */ 440 public void setSMTPHost(String smtpHost) { 441 setSmtpHost(smtpHost); 442 } 443 444 /** 445 * The <b>smtpHost</b> option takes a string value which should be the host 446 * name of the SMTP server that will send the e-mail message. 447 */ 448 public void setSmtpHost(String smtpHost) { 449 this.smtpHost = smtpHost; 450 } 451 452 /** 453 * Alias for getSmtpHost(). 454 */ 455 public String getSMTPHost() { 456 return getSmtpHost(); 457 } 458 459 /** 460 * Returns value of the <b>SMTPHost</b> option. 461 */ 462 public String getSmtpHost() { 463 return smtpHost; 464 } 465 466 /** 467 * Alias for {@link #setSmtpPort}. 468 * 469 * @param port 470 */ 471 public void setSMTPPort(int port) { 472 setSmtpPort(port); 473 } 474 475 /** 476 * The port where the SMTP server is running. Default value is 25. 477 * 478 * @param port 479 */ 480 public void setSmtpPort(int port) { 481 this.smtpPort = port; 482 } 483 484 /** 485 * Alias for {@link #getSmtpPort} 486 * 487 * @return 488 */ 489 public int getSMTPPort() { 490 return getSmtpPort(); 491 } 492 493 /** 494 * See {@link #setSmtpPort} 495 * 496 * @return 497 */ 498 public int getSmtpPort() { 499 return smtpPort; 500 } 501 502 public String getLocalhost() { 503 return localhost; 504 } 505 506 /** 507 * Set the "mail.smtp.localhost" property to the value passed as parameter to 508 * this method. 509 * 510 * <p> 511 * Useful in case the hostname for the client host is not fully qualified and as 512 * a consequence the SMTP server rejects the clients HELO/EHLO command. 513 * </p> 514 * 515 * @param localhost 516 */ 517 public void setLocalhost(String localhost) { 518 this.localhost = localhost; 519 } 520 521 public CyclicBufferTracker<E> getCyclicBufferTracker() { 522 return cbTracker; 523 } 524 525 public void setCyclicBufferTracker(CyclicBufferTracker<E> cbTracker) { 526 this.cbTracker = cbTracker; 527 } 528 529 public Discriminator<E> getDiscriminator() { 530 return discriminator; 531 } 532 533 public void setDiscriminator(Discriminator<E> discriminator) { 534 this.discriminator = discriminator; 535 } 536 537 public boolean isAsynchronousSending() { 538 return asynchronousSending; 539 } 540 541 /** 542 * By default, SMTAppender transmits emails asynchronously. For synchronous 543 * email transmission set asynchronousSending to 'false'. 544 * 545 * @param asynchronousSending determines whether sending is done asynchronously 546 * or not 547 * @since 1.0.4 548 */ 549 public void setAsynchronousSending(boolean asynchronousSending) { 550 this.asynchronousSending = asynchronousSending; 551 } 552 553 public void addTo(String to) { 554 if (to == null || to.length() == 0) { 555 throw new IllegalArgumentException("Null or empty <to> property"); 556 } 557 PatternLayoutBase<E> plb = makeNewToPatternLayout(to.trim()); 558 plb.setContext(context); 559 plb.start(); 560 this.toPatternLayoutList.add(plb); 561 } 562 563 abstract protected PatternLayoutBase<E> makeNewToPatternLayout(String toPattern); 564 565 public List<String> getToAsListOfString() { 566 List<String> toList = new ArrayList<String>(); 567 for (PatternLayoutBase<E> plb : toPatternLayoutList) { 568 toList.add(plb.getPattern()); 569 } 570 return toList; 571 } 572 573 public boolean isSTARTTLS() { 574 return starttls; 575 } 576 577 public void setSTARTTLS(boolean startTLS) { 578 this.starttls = startTLS; 579 } 580 581 public boolean isSSL() { 582 return ssl; 583 } 584 585 public void setSSL(boolean ssl) { 586 this.ssl = ssl; 587 } 588 589 /** 590 * The <b>EventEvaluator</b> option takes a string value representing the name 591 * of the class implementing the {@link EventEvaluator} interface. A 592 * corresponding object will be instantiated and assigned as the event evaluator 593 * for the SMTPAppender. 594 */ 595 public void setEvaluator(EventEvaluator<E> eventEvaluator) { 596 this.eventEvaluator = eventEvaluator; 597 } 598 599 public String getUsername() { 600 return username; 601 } 602 603 public void setUsername(String username) { 604 this.username = username; 605 } 606 607 public String getPassword() { 608 return password; 609 } 610 611 public void setPassword(String password) { 612 this.password = password; 613 } 614 615 /** 616 * @return the charset encoding value 617 * @see #setCharsetEncoding(String) 618 */ 619 public String getCharsetEncoding() { 620 return charsetEncoding; 621 } 622 623 public String getJndiLocation() { 624 return jndiLocation; 625 } 626 627 /** 628 * Set the location where a {@link jakarta.mail.Session} resource is located in 629 * JNDI. Default value is "java:comp/env/mail/Session". 630 * 631 * @param jndiLocation 632 * @since 1.0.6 633 */ 634 public void setJndiLocation(String jndiLocation) { 635 this.jndiLocation = jndiLocation; 636 } 637 638 public boolean isSessionViaJNDI() { 639 return sessionViaJNDI; 640 } 641 642 /** 643 * If set to true, a {@link jakarta.mail.Session} resource will be retrieved from 644 * JNDI. Default is false. 645 * 646 * @param sessionViaJNDI whether to obtain a javax.mail.Session by JNDI 647 * @since 1.0.6 648 */ 649 public void setSessionViaJNDI(boolean sessionViaJNDI) { 650 this.sessionViaJNDI = sessionViaJNDI; 651 } 652 653 /** 654 * Set the character set encoding of the outgoing email messages. The default 655 * encoding is "UTF-8" which usually works well for most purposes. 656 * 657 * @param charsetEncoding 658 */ 659 public void setCharsetEncoding(String charsetEncoding) { 660 this.charsetEncoding = charsetEncoding; 661 } 662 663 public Layout<E> getLayout() { 664 return layout; 665 } 666 667 public void setLayout(Layout<E> layout) { 668 this.layout = layout; 669 } 670 671 class SenderRunnable implements Runnable { 672 673 final CyclicBuffer<E> cyclicBuffer; 674 final E e; 675 676 SenderRunnable(CyclicBuffer<E> cyclicBuffer, E e) { 677 this.cyclicBuffer = cyclicBuffer; 678 this.e = e; 679 } 680 681 public void run() { 682 sendBuffer(cyclicBuffer, e); 683 } 684 } 685}