View Javadoc

1   /**
2    * Logback: the reliable, generic, fast and flexible logging framework.
3    * Copyright (C) 1999-2011, 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.MessagingException;
24  import javax.mail.Multipart;
25  import javax.mail.Session;
26  import javax.mail.Transport;
27  import javax.mail.internet.AddressException;
28  import javax.mail.internet.InternetAddress;
29  import javax.mail.internet.MimeBodyPart;
30  import javax.mail.internet.MimeMessage;
31  import javax.mail.internet.MimeMultipart;
32  
33  import ch.qos.logback.core.AppenderBase;
34  import ch.qos.logback.core.CoreConstants;
35  import ch.qos.logback.core.Layout;
36  import ch.qos.logback.core.boolex.EvaluationException;
37  import ch.qos.logback.core.boolex.EventEvaluator;
38  import ch.qos.logback.core.helpers.CyclicBuffer;
39  import ch.qos.logback.core.pattern.PatternLayoutBase;
40  import ch.qos.logback.core.sift.DefaultDiscriminator;
41  import ch.qos.logback.core.sift.Discriminator;
42  import ch.qos.logback.core.spi.CyclicBufferTracker;
43  import ch.qos.logback.core.spi.CyclicBufferTrackerImpl;
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   * <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 int MAX_DELAY_BETWEEN_STATUS_MESSAGES = 1228800 * CoreConstants.MILLIS_IN_ONE_SECOND;
66  
67    long lastTrackerStatusPrint = 0;
68    int 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  
81    String username;
82    String password;
83    String localhost;
84  
85    boolean asynchronousSending = true;
86  
87    private String charsetEncoding = "UTF-8";
88  
89    protected MimeMessage mimeMsg;
90  
91    protected EventEvaluator<E> eventEvaluator;
92  
93    protected Discriminator<E> discriminator = new DefaultDiscriminator<E>();
94    protected CyclicBufferTracker<E> cbTracker;
95  
96    private int errorCount = 0;
97  
98    /**
99     * return a layout for the subject string as appropriate for the module. If the
100    * subjectStr parameter is null, then a default value for subjectStr should be
101    * used.
102    *
103    * @param subjectStr
104    * @return a layout as appropriate for the module
105    */
106   abstract protected Layout<E> makeSubjectLayout(String subjectStr);
107 
108   /**
109    * Start the appender
110    */
111   public void start() {
112 
113     if (cbTracker == null) {
114       cbTracker = new CyclicBufferTrackerImpl<E>();
115     }
116 
117     Properties props = new Properties(OptionHelper.getSystemProperties());
118     if (smtpHost != null) {
119       props.put("mail.smtp.host", smtpHost);
120     }
121     props.put("mail.smtp.port", Integer.toString(smtpPort));
122 
123     if(localhost != null) {
124       props.put("mail.smtp.localhost", localhost);
125     }
126 
127     LoginAuthenticator loginAuthenticator = null;
128 
129     if (username != null) {
130       loginAuthenticator = new LoginAuthenticator(username, password);
131       props.put("mail.smtp.auth", "true");
132     }
133 
134     if (isSTARTTLS() && isSSL()) {
135       addError("Both SSL and StartTLS cannot be enabled simultaneously");
136     } else {
137       if (isSTARTTLS()) {
138         // see also http://jira.qos.ch/browse/LBCORE-225
139         props.put("mail.smtp.starttls.enable", "true");
140       }
141       if (isSSL()) {
142         String SSL_FACTORY = "javax.net.ssl.SSLSocketFactory";
143         props.put("mail.smtp.socketFactory.port", Integer.toString(smtpPort));
144         props.put("mail.smtp.socketFactory.class", SSL_FACTORY);
145         props.put("mail.smtp.socketFactory.fallback", "true");
146       }
147     }
148 
149     // props.put("mail.debug", "true");
150 
151     Session session = Session.getInstance(props, loginAuthenticator);
152     mimeMsg = new MimeMessage(session);
153 
154     try {
155       if (from != null) {
156         mimeMsg.setFrom(getAddress(from));
157       } else {
158         mimeMsg.setFrom();
159       }
160 
161       subjectLayout = makeSubjectLayout(subjectStr);
162 
163       started = true;
164 
165     } catch (MessagingException e) {
166       addError("Could not activate SMTPAppender options.", e);
167     }
168   }
169 
170   /**
171    * Perform SMTPAppender specific appending actions, delegating some of them to
172    * a subclass and checking if the event triggers an e-mail to be sent.
173    */
174   protected void append(E eventObject) {
175 
176     if (!checkEntryConditions()) {
177       return;
178     }
179 
180     String key = discriminator.getDiscriminatingValue(eventObject);
181     long now = System.currentTimeMillis();
182     final CyclicBuffer<E> cb = cbTracker.getOrCreate(key, now);
183     subAppend(cb, eventObject);
184 
185     cb.asList();
186 
187     try {
188       if (eventEvaluator.evaluate(eventObject)) {
189         // clone the CyclicBuffer before sending out asynchronously
190         CyclicBuffer<E> cbClone = new CyclicBuffer<E>(cb);
191         // see http://jira.qos.ch/browse/LBCLASSIC-221
192         cb.clear();
193 
194         // perform actual sending asynchronously
195         SenderRunnable senderRunnable = new SenderRunnable(cbClone, eventObject);
196         context.getExecutorService().execute(senderRunnable);
197       }
198     } catch (EvaluationException ex) {
199       errorCount++;
200       if (errorCount < CoreConstants.MAX_ERROR_COUNT) {
201         addError("SMTPAppender's EventEvaluator threw an Exception-", ex);
202       }
203     }
204 
205     // immediately remove the buffer if asked by the user
206     if (isEventMarkedForBufferRemoval(eventObject)) {
207       cbTracker.removeBuffer(key);
208     }
209 
210     cbTracker.clearStaleBuffers(now);
211 
212     if (lastTrackerStatusPrint + delayBetweenStatusMessages < now) {
213       addInfo("SMTPAppender [" + name + "] is tracking [" + cbTracker.size() + "] buffers");
214       lastTrackerStatusPrint = now;
215       // quadruple 'delay' assuming less than max delay
216       if (delayBetweenStatusMessages < MAX_DELAY_BETWEEN_STATUS_MESSAGES) {
217         delayBetweenStatusMessages *= 4;
218       }
219     }
220   }
221 
222   abstract protected boolean isEventMarkedForBufferRemoval(E eventObject);
223 
224   abstract protected void subAppend(CyclicBuffer<E> cb, E eventObject);
225 
226   /**
227    * This method determines if there is a sense in attempting to append.
228    * <p/>
229    * <p/>
230    * It checks whether there is a set output target and also if there is a set
231    * layout. If these checks fail, then the boolean value <code>false</code> is
232    * returned.
233    */
234   public boolean checkEntryConditions() {
235     if (!this.started) {
236       addError("Attempting to append to a non-started appender: "
237               + this.getName());
238       return false;
239     }
240 
241     if (this.mimeMsg == null) {
242       addError("Message object not configured.");
243       return false;
244     }
245 
246     if (this.eventEvaluator == null) {
247       addError("No EventEvaluator is set for appender [" + name + "].");
248       return false;
249     }
250 
251     if (this.layout == null) {
252       addError("No layout set for appender named ["
253               + name
254               + "]. For more information, please visit http://logback.qos.ch/codes.html#smtp_no_layout");
255       return false;
256     }
257     return true;
258   }
259 
260   synchronized public void stop() {
261     this.started = false;
262   }
263 
264   InternetAddress getAddress(String addressStr) {
265     try {
266       return new InternetAddress(addressStr);
267     } catch (AddressException e) {
268       addError("Could not parse address [" + addressStr + "].", e);
269       return null;
270     }
271   }
272 
273   private List<InternetAddress> parseAddress(E event) {
274     int len = toPatternLayoutList.size();
275 
276     List<InternetAddress> iaList = new ArrayList<InternetAddress>();
277 
278     for (int i = 0; i < len; i++) {
279       try {
280         PatternLayoutBase<E> emailPL = toPatternLayoutList.get(i);
281         String email = emailPL.doLayout(event);
282         if (email == null || email.length() == 0) {
283           continue;
284         }
285         InternetAddress[] tmp = InternetAddress.parse(email, true);
286         iaList.addAll(Arrays.asList(tmp));
287       } catch (AddressException e) {
288         addError("Could not parse email address for [" + toPatternLayoutList.get(i) + "] for event [" + event + "]", e);
289         return iaList;
290       }
291     }
292 
293     return iaList;
294   }
295 
296   /**
297    * Returns value of the <b>toList</b> option.
298    */
299   public List<PatternLayoutBase<E>> getToList() {
300     return toPatternLayoutList;
301   }
302 
303   /**
304    * Send the contents of the cyclic buffer as an e-mail message.
305    */
306   protected void sendBuffer(CyclicBuffer<E> cb, E lastEventObject) {
307 
308     // Note: this code already owns the monitor for this
309     // appender. This frees us from needing to synchronize on 'cb'.
310     try {
311       MimeBodyPart part = new MimeBodyPart();
312 
313       StringBuffer sbuf = new StringBuffer();
314 
315       String header = layout.getFileHeader();
316       if (header != null) {
317         sbuf.append(header);
318       }
319       String presentationHeader = layout.getPresentationHeader();
320       if (presentationHeader != null) {
321         sbuf.append(presentationHeader);
322       }
323       fillBuffer(cb, sbuf);
324       String presentationFooter = layout.getPresentationFooter();
325       if (presentationFooter != null) {
326         sbuf.append(presentationFooter);
327       }
328       String footer = layout.getFileFooter();
329       if (footer != null) {
330         sbuf.append(footer);
331       }
332 
333       String subjectStr = "Undefined subject";
334       if (subjectLayout != null) {
335         subjectStr = subjectLayout.doLayout(lastEventObject);
336       }
337       mimeMsg.setSubject(subjectStr, charsetEncoding);
338 
339       List<InternetAddress> destinationAddresses = parseAddress(lastEventObject);
340       if (destinationAddresses.isEmpty()) {
341         addInfo("Empty destination address. Aborting email transmission");
342         return;
343       }
344 
345       InternetAddress[] toAddressArray = destinationAddresses.toArray(EMPTY_IA_ARRAY);
346       mimeMsg.setRecipients(Message.RecipientType.TO, toAddressArray);
347 
348       String contentType = layout.getContentType();
349 
350       if (ContentTypeUtil.isTextual(contentType)) {
351         part.setText(sbuf.toString(), charsetEncoding, ContentTypeUtil
352                 .getSubType(contentType));
353       } else {
354         part.setContent(sbuf.toString(), layout.getContentType());
355       }
356 
357       Multipart mp = new MimeMultipart();
358       mp.addBodyPart(part);
359       mimeMsg.setContent(mp);
360 
361       mimeMsg.setSentDate(new Date());
362       Transport.send(mimeMsg);
363       addInfo("Sent out SMTP message \""+subjectStr+"\" to "+Arrays.toString(toAddressArray));
364     } catch (Exception e) {
365       addError("Error occurred while sending e-mail notification.", e);
366     }
367   }
368 
369   abstract protected void fillBuffer(CyclicBuffer<E> cb, StringBuffer sbuf);
370 
371   /**
372    * Returns value of the <b>From</b> option.
373    */
374   public String getFrom() {
375     return from;
376   }
377 
378   /**
379    * Returns value of the <b>Subject</b> option.
380    */
381   public String getSubject() {
382     return subjectStr;
383   }
384 
385   /**
386    * The <b>From</b> option takes a string value which should be a e-mail
387    * address of the sender.
388    */
389   public void setFrom(String from) {
390     this.from = from;
391   }
392 
393   /**
394    * The <b>Subject</b> option takes a string value which should be a the
395    * subject of the e-mail message.
396    */
397   public void setSubject(String subject) {
398     this.subjectStr = subject;
399   }
400 
401   /**
402    * Alias for smtpHost
403    *
404    * @param smtpHost
405    */
406   public void setSMTPHost(String smtpHost) {
407     setSmtpHost(smtpHost);
408   }
409 
410   /**
411    * The <b>smtpHost</b> option takes a string value which should be a the host
412    * name of the SMTP server that will send the e-mail message.
413    */
414   public void setSmtpHost(String smtpHost) {
415     this.smtpHost = smtpHost;
416   }
417 
418   /**
419    * Alias for getSmtpHost().
420    */
421   public String getSMTPHost() {
422     return getSmtpHost();
423   }
424 
425   /**
426    * Returns value of the <b>SMTPHost</b> option.
427    */
428   public String getSmtpHost() {
429     return smtpHost;
430   }
431 
432   /**
433    * Alias for {@link #setSmtpPort}.
434    *
435    * @param port
436    */
437   public void setSMTPPort(int port) {
438     setSmtpPort(port);
439   }
440 
441   /**
442    * The port where the SMTP server is running. Default value is 25.
443    *
444    * @param port
445    */
446   public void setSmtpPort(int port) {
447     this.smtpPort = port;
448   }
449 
450   /**
451    * Alias for {@link #getSmtpPort}
452    *
453    * @return
454    */
455   public int getSMTPPort() {
456     return getSmtpPort();
457   }
458 
459   /**
460    * See {@link #setSmtpPort}
461    *
462    * @return
463    */
464   public int getSmtpPort() {
465     return smtpPort;
466   }
467 
468   public String getLocalhost() {
469     return localhost;
470   }
471 
472   /**
473    * Set the "mail.smtp.localhost" property to the value passed as parameter to
474    * this method.
475    *
476    * <p>Useful in case the hostname for the client host is not fully qualified
477    * and as a consequence the SMTP server rejects the clients HELO/EHLO command.
478    * </p>
479    *
480    * @param localhost
481    */
482   public void setLocalhost(String localhost) {
483     this.localhost = localhost;
484   }
485 
486   public CyclicBufferTracker<E> getCyclicBufferTracker() {
487     return cbTracker;
488   }
489 
490   public void setCyclicBufferTracker(CyclicBufferTracker<E> cbTracker) {
491     this.cbTracker = cbTracker;
492   }
493 
494   public Discriminator<E> getDiscriminator() {
495     return discriminator;
496   }
497 
498   public void setDiscriminator(Discriminator<E> discriminator) {
499     this.discriminator = discriminator;
500   }
501 
502   public void addTo(String to) {
503     if (to == null || to.length() == 0) {
504       throw new IllegalArgumentException("Null or empty <to> property");
505     }
506     PatternLayoutBase plb = makeNewToPatternLayout(to.trim());
507     plb.setContext(context);
508     plb.start();
509     this.toPatternLayoutList.add(plb);
510   }
511 
512   abstract protected PatternLayoutBase<E> makeNewToPatternLayout(String toPattern);
513 
514   public List<String> getToAsListOfString() {
515     List<String> toList = new ArrayList<String>();
516     for (PatternLayoutBase plb : toPatternLayoutList) {
517       toList.add(plb.getPattern());
518     }
519     return toList;
520   }
521 
522   // for testing purpose only
523   public Message getMessage() {
524     return mimeMsg;
525   }
526 
527   // for testing purpose only
528 
529   public void setMessage(MimeMessage msg) {
530     this.mimeMsg = msg;
531   }
532 
533   public boolean isSTARTTLS() {
534     return starttls;
535   }
536 
537   public void setSTARTTLS(boolean startTLS) {
538     this.starttls = startTLS;
539   }
540 
541   public boolean isSSL() {
542     return ssl;
543   }
544 
545   public void setSSL(boolean ssl) {
546     this.ssl = ssl;
547   }
548 
549   /**
550    * The <b>EventEvaluator</b> option takes a string value representing the name
551    * of the class implementing the {@link EventEvaluator} interface. A
552    * corresponding object will be instantiated and assigned as the event
553    * evaluator for the SMTPAppender.
554    */
555   public void setEvaluator(EventEvaluator<E> eventEvaluator) {
556     this.eventEvaluator = eventEvaluator;
557   }
558 
559   public String getUsername() {
560     return username;
561   }
562 
563   public void setUsername(String username) {
564     this.username = username;
565   }
566 
567   public String getPassword() {
568     return password;
569   }
570 
571   public void setPassword(String password) {
572     this.password = password;
573   }
574 
575   /**
576    * @return the charset encoding value
577    * @see #setCharsetEncoding(String)
578    */
579   public String getCharsetEncoding() {
580     return charsetEncoding;
581   }
582 
583   /**
584    * Set the character set encoding of the outgoing email messages. The default
585    * encoding is "UTF-8" which usually works well for most purposes.
586    *
587    * @param charsetEncoding
588    */
589   public void setCharsetEncoding(String charsetEncoding) {
590     this.charsetEncoding = charsetEncoding;
591   }
592 
593   public Layout<E> getLayout() {
594     return layout;
595   }
596 
597   public void setLayout(Layout<E> layout) {
598     this.layout = layout;
599   }
600 
601   class SenderRunnable implements Runnable {
602 
603     final  CyclicBuffer<E> cyclicBuffer;
604     final E e;
605 
606     SenderRunnable(CyclicBuffer<E> cyclicBuffer, E e) {
607       this.cyclicBuffer = cyclicBuffer;
608       this.e = e;
609     }
610     public void run() {
611       sendBuffer(cyclicBuffer, e);
612     }
613   }
614 }