View Javadoc
1   /**
2    * Logback: the reliable, generic, fast and flexible logging framework.
3    * Copyright (C) 1999-2015, QOS.ch. All rights reserved.
4    *
5    * This program and the accompanying materials are dual-licensed under
6    * either the terms of the Eclipse Public License v1.0 as published by
7    * the Eclipse Foundation
8    *
9    *   or (per the licensee's choosing)
10   *
11   * under the terms of the GNU Lesser General Public License version 2.1
12   * as published by the Free Software Foundation.
13   */
14  package ch.qos.logback.core.net;
15  
16  import java.util.ArrayList;
17  import java.util.Arrays;
18  import java.util.Date;
19  import java.util.List;
20  import java.util.Properties;
21  import java.util.concurrent.Future;
22  
23  import jakarta.mail.Message;
24  import jakarta.mail.Multipart;
25  import jakarta.mail.Session;
26  import jakarta.mail.Transport;
27  import jakarta.mail.internet.AddressException;
28  import jakarta.mail.internet.InternetAddress;
29  import jakarta.mail.internet.MimeBodyPart;
30  import jakarta.mail.internet.MimeMessage;
31  import jakarta.mail.internet.MimeMultipart;
32  import javax.naming.Context;
33  
34  import ch.qos.logback.core.AppenderBase;
35  import ch.qos.logback.core.CoreConstants;
36  import ch.qos.logback.core.Layout;
37  import ch.qos.logback.core.boolex.EvaluationException;
38  import ch.qos.logback.core.boolex.EventEvaluator;
39  import ch.qos.logback.core.helpers.CyclicBuffer;
40  import ch.qos.logback.core.pattern.PatternLayoutBase;
41  import ch.qos.logback.core.sift.DefaultDiscriminator;
42  import ch.qos.logback.core.sift.Discriminator;
43  import ch.qos.logback.core.spi.CyclicBufferTracker;
44  import ch.qos.logback.core.util.ContentTypeUtil;
45  import ch.qos.logback.core.util.JNDIUtil;
46  import ch.qos.logback.core.util.OptionHelper;
47  
48  // Contributors:
49  // Andrey Rybin charset encoding support http://jira.qos.ch/browse/LBCORE-69
50  
51  /**
52   * An abstract class that provides support for sending events to an email
53   * address.
54   * <p>
55   * See http://logback.qos.ch/manual/appenders.html#SMTPAppender for further
56   * documentation.
57   *
58   * @author Ceki G&uuml;lc&uuml;
59   * @author S&eacute;bastien Pennec
60   */
61  public abstract class SMTPAppenderBase<E> extends AppenderBase<E> {
62  
63      static InternetAddress[] EMPTY_IA_ARRAY = new InternetAddress[0];
64      // ~ 14 days
65      static final long MAX_DELAY_BETWEEN_STATUS_MESSAGES = 1228800 * CoreConstants.MILLIS_IN_ONE_SECOND;
66  
67      long lastTrackerStatusPrint = 0;
68      long delayBetweenStatusMessages = 300 * CoreConstants.MILLIS_IN_ONE_SECOND;
69  
70      protected Layout<E> subjectLayout;
71      protected Layout<E> layout;
72  
73      private List<PatternLayoutBase<E>> toPatternLayoutList = new ArrayList<PatternLayoutBase<E>>();
74      private String from;
75      private String subjectStr = null;
76      private String smtpHost;
77      private int smtpPort = 25;
78      private boolean starttls = false;
79      private boolean ssl = false;
80      private boolean sessionViaJNDI = false;
81      private String jndiLocation = CoreConstants.JNDI_COMP_PREFIX + "/mail/Session";
82  
83      String username;
84      String password;
85      String localhost;
86  
87      boolean asynchronousSending = true;
88      protected Future<?> asynchronousSendingFuture = null;
89      private String charsetEncoding = "UTF-8";
90  
91      protected Session session;
92  
93      protected EventEvaluator<E> eventEvaluator;
94  
95      protected Discriminator<E> discriminator = new DefaultDiscriminator<E>();
96      protected CyclicBufferTracker<E> cbTracker;
97  
98      private int errorCount = 0;
99  
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 }