View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    * 
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   * 
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.log4j.net;
19  
20  import org.apache.log4j.AppenderSkeleton;
21  import org.apache.log4j.Layout;
22  import org.apache.log4j.Level;
23  import org.apache.log4j.helpers.CyclicBuffer;
24  import org.apache.log4j.helpers.LogLog;
25  import org.apache.log4j.helpers.OptionConverter;
26  import org.apache.log4j.spi.ErrorCode;
27  import org.apache.log4j.spi.LoggingEvent;
28  import org.apache.log4j.spi.OptionHandler;
29  import org.apache.log4j.spi.TriggeringEventEvaluator;
30  import org.apache.log4j.xml.UnrecognizedElementHandler;
31  import org.w3c.dom.Element;
32  
33  import javax.mail.Authenticator;
34  import javax.mail.Message;
35  import javax.mail.MessagingException;
36  import javax.mail.Multipart;
37  import javax.mail.PasswordAuthentication;
38  import javax.mail.Session;
39  import javax.mail.Transport;
40  import javax.mail.internet.AddressException;
41  import javax.mail.internet.InternetAddress;
42  import javax.mail.internet.InternetHeaders;
43  import javax.mail.internet.MimeBodyPart;
44  import javax.mail.internet.MimeMessage;
45  import javax.mail.internet.MimeMultipart;
46  import javax.mail.internet.MimeUtility;
47  import java.io.ByteArrayOutputStream;
48  import java.io.OutputStreamWriter;
49  import java.io.UnsupportedEncodingException;
50  import java.io.Writer;
51  import java.util.Date;
52  import java.util.Properties;
53  
54  /**
55     Send an e-mail when a specific logging event occurs, typically on
56     errors or fatal errors.
57  
58     <p>The number of logging events delivered in this e-mail depend on
59     the value of <b>BufferSize</b> option. The
60     <code>SMTPAppender</code> keeps only the last
61     <code>BufferSize</code> logging events in its cyclic buffer. This
62     keeps memory requirements at a reasonable level while still
63     delivering useful application context.
64  
65     By default, an email message will be sent when an ERROR or higher
66     severity message is appended.  The triggering criteria can be
67     modified by setting the evaluatorClass property with the name
68     of a class implementing TriggeringEventEvaluator, setting the evaluator
69     property with an instance of TriggeringEventEvaluator or
70     nesting a triggeringPolicy element where the specified
71     class implements TriggeringEventEvaluator.
72     
73     This class has implemented UnrecognizedElementHandler since 1.2.15.
74  
75     Since 1.2.16, SMTP over SSL is supported by setting SMTPProtocol to "smpts".
76  
77     @author Ceki G&uuml;lc&uuml;
78     @since 1.0 */
79  public class SMTPAppender extends AppenderSkeleton
80          implements UnrecognizedElementHandler {
81    private String to;
82    /**
83     * Comma separated list of cc recipients.
84     */
85    private String cc;  
86    /**
87     * Comma separated list of bcc recipients.
88     */
89    private String bcc;  
90    private String from;
91    /**
92     * Comma separated list of replyTo addresses.
93     */
94    private String replyTo;
95    private String subject;
96    private String smtpHost;
97    private String smtpUsername;
98    private String smtpPassword;
99    private String smtpProtocol;
100   private int smtpPort = -1;
101   private boolean smtpDebug = false;
102   private int bufferSize = 512;
103   private boolean locationInfo = false;
104   private boolean sendOnClose = false;
105 
106   protected CyclicBuffer cb = new CyclicBuffer(bufferSize);
107   protected Message msg;
108 
109   protected TriggeringEventEvaluator evaluator;
110 
111 
112 
113   /**
114      The default constructor will instantiate the appender with a
115      {@link TriggeringEventEvaluator} that will trigger on events with
116      level ERROR or higher.*/
117   public
118   SMTPAppender() {
119     this(new DefaultEvaluator());
120   }
121 
122 
123   /**
124      Use <code>evaluator</code> passed as parameter as the {@link
125      TriggeringEventEvaluator} for this SMTPAppender.  */
126   public
127   SMTPAppender(TriggeringEventEvaluator evaluator) {
128     this.evaluator = evaluator;
129   }
130 
131 
132   /**
133      Activate the specified options, such as the smtp host, the
134      recipient, from, etc. */
135   public
136   void activateOptions() {
137     Session session = createSession();
138     msg = new MimeMessage(session);
139 
140      try {
141         addressMessage(msg);
142         if(subject != null) {
143            try {
144                 msg.setSubject(MimeUtility.encodeText(subject, "UTF-8", null));
145            } catch(UnsupportedEncodingException ex) {
146                 LogLog.error("Unable to encode SMTP subject", ex);
147            }
148         }
149      } catch(MessagingException e) {
150        LogLog.error("Could not activate SMTPAppender options.", e );
151      }
152 
153      if (evaluator instanceof OptionHandler) {
154          ((OptionHandler) evaluator).activateOptions();
155      }
156   }
157   
158   /**
159    *   Address message.
160    *   @param msg message, may not be null.
161    *   @throws MessagingException thrown if error addressing message. 
162    *   @since 1.2.14
163    */
164   protected void addressMessage(final Message msg) throws MessagingException {
165        if (from != null) {
166 	 		msg.setFrom(getAddress(from));
167        } else {
168 	 		msg.setFrom();
169 	   }
170 
171       //Add ReplyTo addresses if defined.
172          if (replyTo != null && replyTo.length() > 0) {
173                msg.setReplyTo(parseAddress(replyTo));
174          }
175 
176        if (to != null && to.length() > 0) {
177              msg.setRecipients(Message.RecipientType.TO, parseAddress(to));
178        }
179 
180       //Add CC receipients if defined.
181 	  if (cc != null && cc.length() > 0) {
182 		msg.setRecipients(Message.RecipientType.CC, parseAddress(cc));
183 	  }
184 
185       //Add BCC receipients if defined.
186 	  if (bcc != null && bcc.length() > 0) {
187 		msg.setRecipients(Message.RecipientType.BCC, parseAddress(bcc));
188 	  }
189   }
190   
191   /**
192    *  Create mail session.
193    *  @return mail session, may not be null.
194    *  @since 1.2.14
195    */
196   protected Session createSession() {
197     Properties props = null;
198     try {
199         props = new Properties (System.getProperties());
200     } catch(SecurityException ex) {
201         props = new Properties();
202     }
203 
204     String prefix = "mail.smtp";
205     if (smtpProtocol != null) {
206         props.put("mail.transport.protocol", smtpProtocol);
207         prefix = "mail." + smtpProtocol;
208     }
209     if (smtpHost != null) {
210       props.put(prefix + ".host", smtpHost);
211     }
212     if (smtpPort > 0) {
213         props.put(prefix + ".port", String.valueOf(smtpPort));
214     }
215     
216     Authenticator auth = null;
217     if(smtpPassword != null && smtpUsername != null) {
218       props.put(prefix + ".auth", "true");
219       auth = new Authenticator() {
220         protected PasswordAuthentication getPasswordAuthentication() {
221           return new PasswordAuthentication(smtpUsername, smtpPassword);
222         }
223       };
224     }
225     Session session = Session.getInstance(props, auth);
226     if (smtpProtocol != null) {
227         session.setProtocolForAddress("rfc822", smtpProtocol);
228     }
229     if (smtpDebug) {
230         session.setDebug(smtpDebug);
231     }
232     return session;
233   }
234 
235   /**
236      Perform SMTPAppender specific appending actions, mainly adding
237      the event to a cyclic buffer and checking if the event triggers
238      an e-mail to be sent. */
239   public
240   void append(LoggingEvent event) {
241 
242     if(!checkEntryConditions()) {
243       return;
244     }
245 
246     event.getThreadName();
247     event.getNDC();
248     event.getMDCCopy();
249     if(locationInfo) {
250       event.getLocationInformation();
251     }
252     event.getRenderedMessage();
253     event.getThrowableStrRep();
254     cb.add(event);
255     if(evaluator.isTriggeringEvent(event)) {
256       sendBuffer();
257     }
258   }
259 
260  /**
261      This method determines if there is a sense in attempting to append.
262 
263      <p>It checks whether there is a set output target and also if
264      there is a set layout. If these checks fail, then the boolean
265      value <code>false</code> is returned. */
266   protected
267   boolean checkEntryConditions() {
268     if(this.msg == null) {
269       errorHandler.error("Message object not configured.");
270       return false;
271     }
272 
273     if(this.evaluator == null) {
274       errorHandler.error("No TriggeringEventEvaluator is set for appender ["+
275 			 name+"].");
276       return false;
277     }
278 
279 
280     if(this.layout == null) {
281       errorHandler.error("No layout set for appender named ["+name+"].");
282       return false;
283     }
284     return true;
285   }
286 
287 
288   synchronized
289   public
290   void close() {
291     this.closed = true;
292     if (sendOnClose && cb.length() > 0) {
293         sendBuffer();
294     }
295   }
296 
297   InternetAddress getAddress(String addressStr) {
298     try {
299       return new InternetAddress(addressStr);
300     } catch(AddressException e) {
301       errorHandler.error("Could not parse address ["+addressStr+"].", e,
302 			 ErrorCode.ADDRESS_PARSE_FAILURE);
303       return null;
304     }
305   }
306 
307   InternetAddress[] parseAddress(String addressStr) {
308     try {
309       return InternetAddress.parse(addressStr, true);
310     } catch(AddressException e) {
311       errorHandler.error("Could not parse address ["+addressStr+"].", e,
312 			 ErrorCode.ADDRESS_PARSE_FAILURE);
313       return null;
314     }
315   }
316 
317   /**
318      Returns value of the <b>To</b> option.
319    */
320   public
321   String getTo() {
322     return to;
323   }
324 
325 
326   /**
327      The <code>SMTPAppender</code> requires a {@link
328      org.apache.log4j.Layout layout}.  */
329   public
330   boolean requiresLayout() {
331     return true;
332   }
333 
334   /**
335    * Layout body of email message.
336    * @since 1.2.16  
337    */
338   protected String formatBody() {
339 	  
340 	  // Note: this code already owns the monitor for this
341 	  // appender. This frees us from needing to synchronize on 'cb'.
342 	  
343       StringBuffer sbuf = new StringBuffer();
344       String t = layout.getHeader();
345       if(t != null)
346 	sbuf.append(t);
347       int len =  cb.length();
348       for(int i = 0; i < len; i++) {
349 	//sbuf.append(MimeUtility.encodeText(layout.format(cb.get())));
350 	LoggingEvent event = cb.get();
351 	sbuf.append(layout.format(event));
352 	if(layout.ignoresThrowable()) {
353 	  String[] s = event.getThrowableStrRep();
354 	  if (s != null) {
355 	    for(int j = 0; j < s.length; j++) {
356 	      sbuf.append(s[j]);
357 	      sbuf.append(Layout.LINE_SEP);
358 	    }
359 	  }
360 	}
361       }
362       t = layout.getFooter();
363       if(t != null) {
364 	    sbuf.append(t);
365       }
366       
367       return sbuf.toString();
368   }
369   
370   /**
371      Send the contents of the cyclic buffer as an e-mail message.
372    */
373   protected
374   void sendBuffer() {
375 
376     try {
377       String s = formatBody();
378       boolean allAscii = true;
379       for(int i = 0; i < s.length() && allAscii; i++) {
380           allAscii = s.charAt(i) <= 0x7F;
381       }
382       MimeBodyPart part;
383       if (allAscii) {
384           part = new MimeBodyPart();
385           part.setContent(s, layout.getContentType());
386       } else {
387           try {
388             ByteArrayOutputStream os = new ByteArrayOutputStream();
389             Writer writer = new OutputStreamWriter(
390                     MimeUtility.encode(os, "quoted-printable"), "UTF-8");
391             writer.write(s);
392             writer.close();
393             InternetHeaders headers = new InternetHeaders();
394             headers.setHeader("Content-Type", layout.getContentType() + "; charset=UTF-8");
395             headers.setHeader("Content-Transfer-Encoding", "quoted-printable");
396             part = new MimeBodyPart(headers, os.toByteArray());
397           } catch(Exception ex) {
398               StringBuffer sbuf = new StringBuffer(s);
399               for (int i = 0; i < sbuf.length(); i++) {
400                   if (sbuf.charAt(i) >= 0x80) {
401                       sbuf.setCharAt(i, '?');
402                   }
403               }
404               part = new MimeBodyPart();
405               part.setContent(sbuf.toString(), layout.getContentType());
406           }
407       }
408 
409 
410 
411       Multipart mp = new MimeMultipart();
412       mp.addBodyPart(part);
413       msg.setContent(mp);
414 
415       msg.setSentDate(new Date());
416       Transport.send(msg);
417     } catch(MessagingException e) {
418       LogLog.error("Error occured while sending e-mail notification.", e);
419     } catch(RuntimeException e) {
420       LogLog.error("Error occured while sending e-mail notification.", e);
421     }
422   }
423 
424 
425 
426   /**
427      Returns value of the <b>EvaluatorClass</b> option.
428    */
429   public
430   String getEvaluatorClass() {
431     return evaluator == null ? null : evaluator.getClass().getName();
432   }
433 
434   /**
435      Returns value of the <b>From</b> option.
436    */
437   public
438   String getFrom() {
439     return from;
440   }
441 
442   /**
443      Get the reply addresses.
444      @return reply addresses as comma separated string, may be null.
445      @since 1.2.16
446    */
447   public
448   String getReplyTo() {
449     return replyTo;
450   }
451 
452   /**
453      Returns value of the <b>Subject</b> option.
454    */
455   public
456   String getSubject() {
457     return subject;
458   }
459 
460   /**
461      The <b>From</b> option takes a string value which should be a
462      e-mail address of the sender.
463    */
464   public
465   void setFrom(String from) {
466     this.from = from;
467   }
468 
469   /**
470      Set the e-mail addresses to which replies should be directed.
471      @param addresses reply addresses as comma separated string, may be null.
472      @since 1.2.16
473    */
474   public
475   void setReplyTo(final String addresses) {
476     this.replyTo = addresses;
477   }
478 
479 
480   /**
481      The <b>Subject</b> option takes a string value which should be a
482      the subject of the e-mail message.
483    */
484   public
485   void setSubject(String subject) {
486     this.subject = subject;
487   }
488 
489 
490   /**
491      The <b>BufferSize</b> option takes a positive integer
492      representing the maximum number of logging events to collect in a
493      cyclic buffer. When the <code>BufferSize</code> is reached,
494      oldest events are deleted as new events are added to the
495      buffer. By default the size of the cyclic buffer is 512 events.
496    */
497   public
498   void setBufferSize(int bufferSize) {
499     this.bufferSize = bufferSize;
500     cb.resize(bufferSize);
501   }
502 
503   /**
504      The <b>SMTPHost</b> option takes a string value which should be a
505      the host name of the SMTP server that will send the e-mail message.
506    */
507   public
508   void setSMTPHost(String smtpHost) {
509     this.smtpHost = smtpHost;
510   }
511 
512   /**
513      Returns value of the <b>SMTPHost</b> option.
514    */
515   public
516   String getSMTPHost() {
517     return smtpHost;
518   }
519 
520   /**
521      The <b>To</b> option takes a string value which should be a
522      comma separated list of e-mail address of the recipients.
523    */
524   public
525   void setTo(String to) {
526     this.to = to;
527   }
528 
529 
530 
531   /**
532      Returns value of the <b>BufferSize</b> option.
533    */
534   public
535   int getBufferSize() {
536     return bufferSize;
537   }
538 
539   /**
540      The <b>EvaluatorClass</b> option takes a string value
541      representing the name of the class implementing the {@link
542      TriggeringEventEvaluator} interface. A corresponding object will
543      be instantiated and assigned as the triggering event evaluator
544      for the SMTPAppender.
545    */
546   public
547   void setEvaluatorClass(String value) {
548       evaluator = (TriggeringEventEvaluator)
549                 OptionConverter.instantiateByClassName(value,
550 					   TriggeringEventEvaluator.class,
551 						       evaluator);
552   }
553 
554 
555   /**
556      The <b>LocationInfo</b> option takes a boolean value. By
557      default, it is set to false which means there will be no effort
558      to extract the location information related to the event. As a
559      result, the layout that formats the events as they are sent out
560      in an e-mail is likely to place the wrong location information
561      (if present in the format).
562 
563      <p>Location information extraction is comparatively very slow and
564      should be avoided unless performance is not a concern.
565    */
566   public
567   void setLocationInfo(boolean locationInfo) {
568     this.locationInfo = locationInfo;
569   }
570 
571   /**
572      Returns value of the <b>LocationInfo</b> option.
573    */
574   public
575   boolean getLocationInfo() {
576     return locationInfo;
577   }
578   
579    /**
580       Set the cc recipient addresses.
581       @param addresses recipient addresses as comma separated string, may be null.
582       @since 1.2.14
583     */
584    public void setCc(final String addresses) {
585      this.cc = addresses;
586    }
587 
588    /**
589       Get the cc recipient addresses.
590       @return recipient addresses as comma separated string, may be null.
591       @since 1.2.14
592     */
593     public String getCc() {
594      return cc;
595     }
596 
597    /**
598       Set the bcc recipient addresses.
599       @param addresses recipient addresses as comma separated string, may be null.
600       @since 1.2.14
601     */
602    public void setBcc(final String addresses) {
603      this.bcc = addresses;
604    }
605 
606    /**
607       Get the bcc recipient addresses.
608       @return recipient addresses as comma separated string, may be null.
609       @since 1.2.14
610     */
611     public String getBcc() {
612      return bcc;
613     }
614 
615   /**
616    * The <b>SmtpPassword</b> option takes a string value which should be the password required to authenticate against
617    * the mail server.
618    * @param password password, may be null.
619    * @since 1.2.14
620    */
621   public void setSMTPPassword(final String password) {
622     this.smtpPassword = password;
623   }
624  
625   /**
626    * The <b>SmtpUsername</b> option takes a string value which should be the username required to authenticate against
627    * the mail server.
628    * @param username user name, may be null.
629    * @since 1.2.14
630    */
631   public void setSMTPUsername(final String username) {
632     this.smtpUsername = username;
633   }
634 
635   /**
636    * Setting the <b>SmtpDebug</b> option to true will cause the mail session to log its server interaction to stdout.
637    * This can be useful when debuging the appender but should not be used during production because username and
638    * password information is included in the output.
639    * @param debug debug flag.
640    * @since 1.2.14
641    */
642   public void setSMTPDebug(final boolean debug) {
643     this.smtpDebug = debug;
644   }
645   
646   /**
647    * Get SMTP password.
648    * @return SMTP password, may be null.
649    * @since 1.2.14
650    */
651   public String getSMTPPassword() {
652     return smtpPassword;
653   }
654  
655   /**
656    * Get SMTP user name.
657    * @return SMTP user name, may be null.
658    * @since 1.2.14
659    */
660   public String getSMTPUsername() {
661     return smtpUsername;
662   }
663 
664   /**
665    * Get SMTP debug.
666    * @return SMTP debug flag.
667    * @since 1.2.14
668    */
669   public boolean getSMTPDebug() {
670     return smtpDebug;
671   }
672 
673     /**
674      * Sets triggering evaluator.
675      * @param trigger triggering event evaluator.
676      * @since 1.2.15
677      */
678   public final void setEvaluator(final TriggeringEventEvaluator trigger) {
679       if (trigger == null) {
680           throw new NullPointerException("trigger");
681       }
682       this.evaluator = trigger;
683   }
684 
685     /**
686      * Get triggering evaluator.
687      * @return triggering event evaluator.
688      * @since 1.2.15
689      */
690   public final TriggeringEventEvaluator getEvaluator() {
691       return evaluator;
692   }
693 
694   /** {@inheritDoc}
695    * @since 1.2.15 
696   */
697   public boolean parseUnrecognizedElement(final Element element,
698                                           final Properties props) throws Exception {
699       if ("triggeringPolicy".equals(element.getNodeName())) {
700           Object triggerPolicy =
701                   org.apache.log4j.xml.DOMConfigurator.parseElement(
702                           element, props, TriggeringEventEvaluator.class);
703           if (triggerPolicy instanceof TriggeringEventEvaluator) {
704               setEvaluator((TriggeringEventEvaluator) triggerPolicy);
705           }
706           return true;
707       }
708 
709       return false;
710   }
711 
712     /**
713      * Get transport protocol.
714      * Typically null or "smtps".
715      *
716      * @return transport protocol, may be null.
717      * @since 1.2.16
718      */
719   public final String getSMTPProtocol() {
720       return smtpProtocol;
721   }
722 
723     /**
724      * Set transport protocol.
725      * Typically null or "smtps".
726      *
727      * @param val transport protocol, may be null.
728      * @since 1.2.16
729      */
730   public final void setSMTPProtocol(final String val) {
731       smtpProtocol = val;
732   }
733 
734     /**
735      * Get port.
736      *
737      * @return port, negative values indicate use of default ports for protocol.
738      * @since 1.2.16
739      */
740   public final int getSMTPPort() {
741         return smtpPort;
742   }
743 
744     /**
745      * Set port.
746      *
747      * @param val port, negative values indicate use of default ports for protocol.
748      * @since 1.2.16
749      */
750   public final void setSMTPPort(final int val) {
751         smtpPort = val;
752   }
753 
754     /**
755      * Get sendOnClose.
756      *
757      * @return if true all buffered logging events will be sent when the appender is closed.
758      * @since 1.2.16
759      */
760   public final boolean getSendOnClose() {
761         return sendOnClose;
762   }
763 
764     /**
765      * Set sendOnClose.
766      *
767      * @param val if true all buffered logging events will be sent when appender is closed.
768      * @since 1.2.16
769      */
770   public final void setSendOnClose(final boolean val) {
771         sendOnClose = val;
772   }
773 
774 }
775 
776 class DefaultEvaluator implements TriggeringEventEvaluator {
777   /**
778      Is this <code>event</code> the e-mail triggering event?
779 
780      <p>This method returns <code>true</code>, if the event level
781      has ERROR level or higher. Otherwise it returns
782      <code>false</code>. */
783   public
784   boolean isTriggeringEvent(LoggingEvent event) {
785     return event.getLevel().isGreaterOrEqual(Level.ERROR);
786   }
787 }