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      public static final String MAIL_SMTP_HOST_PK = "mail.smtp.host";
64      public static final String MAIL_SMTP_PORT_PK = "mail.smtp.port";
65      public static final String MAIL_SMTP_LOCALHOST_PK = "mail.smtp.localhost";
66      public static final String MAIL_SMTP_AUTH_PK = "mail.smtp.auth";
67      public static final String MAIL_SMTP_STARTTLS_ENABLE_PK = "mail.smtp.starttls.enable";
68      public static final String MAIL_TRANSPORT_PROTOCOL_PK = "mail.transport.protocol";
69      public static final String MAIL_SMTP_SSL_ENABLE_PK = "mail.smtp.ssl.enable";
70      static InternetAddress[] EMPTY_IA_ARRAY = new InternetAddress[0];
71      // ~ 14 days
72      static final long MAX_DELAY_BETWEEN_STATUS_MESSAGES = 1228800 * CoreConstants.MILLIS_IN_ONE_SECOND;
73  
74      long lastTrackerStatusPrint = 0;
75      long delayBetweenStatusMessages = 300 * CoreConstants.MILLIS_IN_ONE_SECOND;
76  
77      protected Layout<E> subjectLayout;
78      protected Layout<E> layout;
79  
80      private List<PatternLayoutBase<E>> toPatternLayoutList = new ArrayList<PatternLayoutBase<E>>();
81      private String from;
82      private String subjectStr = null;
83      private String smtpHost;
84      private int smtpPort = 25;
85      private boolean starttls = false;
86      private boolean ssl = false;
87      private boolean sessionViaJNDI = false;
88      private String jndiLocation = CoreConstants.JNDI_COMP_PREFIX + "/mail/Session";
89  
90      String username;
91      String password;
92      String localhost;
93  
94      boolean asynchronousSending = true;
95      protected Future<?> asynchronousSendingFuture = null;
96      private String charsetEncoding = "UTF-8";
97  
98      protected Session session;
99  
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 }