001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 * 
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 * 
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.apache.log4j.net;
019
020import org.apache.log4j.AppenderSkeleton;
021import org.apache.log4j.Layout;
022import org.apache.log4j.Level;
023import org.apache.log4j.helpers.CyclicBuffer;
024import org.apache.log4j.helpers.LogLog;
025import org.apache.log4j.helpers.OptionConverter;
026import org.apache.log4j.spi.ErrorCode;
027import org.apache.log4j.spi.LoggingEvent;
028import org.apache.log4j.spi.OptionHandler;
029import org.apache.log4j.spi.TriggeringEventEvaluator;
030import org.apache.log4j.xml.UnrecognizedElementHandler;
031import org.w3c.dom.Element;
032
033import javax.mail.Authenticator;
034import javax.mail.Message;
035import javax.mail.MessagingException;
036import javax.mail.Multipart;
037import javax.mail.PasswordAuthentication;
038import javax.mail.Session;
039import javax.mail.Transport;
040import javax.mail.internet.AddressException;
041import javax.mail.internet.InternetAddress;
042import javax.mail.internet.InternetHeaders;
043import javax.mail.internet.MimeBodyPart;
044import javax.mail.internet.MimeMessage;
045import javax.mail.internet.MimeMultipart;
046import javax.mail.internet.MimeUtility;
047import java.io.ByteArrayOutputStream;
048import java.io.OutputStreamWriter;
049import java.io.UnsupportedEncodingException;
050import java.io.Writer;
051import java.util.Date;
052import java.util.Properties;
053
054/**
055   Send an e-mail when a specific logging event occurs, typically on
056   errors or fatal errors.
057
058   <p>The number of logging events delivered in this e-mail depend on
059   the value of <b>BufferSize</b> option. The
060   <code>SMTPAppender</code> keeps only the last
061   <code>BufferSize</code> logging events in its cyclic buffer. This
062   keeps memory requirements at a reasonable level while still
063   delivering useful application context.
064
065   By default, an email message will be sent when an ERROR or higher
066   severity message is appended.  The triggering criteria can be
067   modified by setting the evaluatorClass property with the name
068   of a class implementing TriggeringEventEvaluator, setting the evaluator
069   property with an instance of TriggeringEventEvaluator or
070   nesting a triggeringPolicy element where the specified
071   class implements TriggeringEventEvaluator.
072   
073   This class has implemented UnrecognizedElementHandler since 1.2.15.
074
075   Since 1.2.16, SMTP over SSL is supported by setting SMTPProtocol to "smpts".
076
077   @author Ceki G&uuml;lc&uuml;
078   @since 1.0 */
079public class SMTPAppender extends AppenderSkeleton
080        implements UnrecognizedElementHandler {
081  private String to;
082  /**
083   * Comma separated list of cc recipients.
084   */
085  private String cc;  
086  /**
087   * Comma separated list of bcc recipients.
088   */
089  private String bcc;  
090  private String from;
091  /**
092   * Comma separated list of replyTo addresses.
093   */
094  private String replyTo;
095  private String subject;
096  private String smtpHost;
097  private String smtpUsername;
098  private String smtpPassword;
099  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
776class 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}