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