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