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 }