View Javadoc

1   /*
2    * ============================================================================
3    *                   The Apache Software License, Version 1.1
4    * ============================================================================
5    *
6    *    Copyright (C) 1999 The Apache Software Foundation. All rights reserved.
7    *
8    * Redistribution and use in source and binary forms, with or without modifica-
9    * tion, are permitted provided that the following conditions are met:
10   *
11   * 1. Redistributions of  source code must  retain the above copyright  notice,
12   *    this list of conditions and the following disclaimer.
13   *
14   * 2. Redistributions in binary form must reproduce the above copyright notice,
15   *    this list of conditions and the following disclaimer in the documentation
16   *    and/or other materials provided with the distribution.
17   *
18   * 3. The end-user documentation included with the redistribution, if any, must
19   *    include  the following  acknowledgment:  "This product includes  software
20   *    developed  by the  Apache Software Foundation  (http://www.apache.org/)."
21   *    Alternately, this  acknowledgment may  appear in the software itself,  if
22   *    and wherever such third-party acknowledgments normally appear.
23   *
24   * 4. The names "log4j" and  "Apache Software Foundation"  must not be used to
25   *    endorse  or promote  products derived  from this  software without  prior
26   *    written permission. For written permission, please contact
27   *    apache@apache.org.
28   *
29   * 5. Products  derived from this software may not  be called "Apache", nor may
30   *    "Apache" appear  in their name,  without prior written permission  of the
31   *    Apache Software Foundation.
32   *
33   * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
34   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
35   * FITNESS  FOR A PARTICULAR  PURPOSE ARE  DISCLAIMED.  IN NO  EVENT SHALL  THE
36   * APACHE SOFTWARE  FOUNDATION  OR ITS CONTRIBUTORS  BE LIABLE FOR  ANY DIRECT,
37   * INDIRECT, INCIDENTAL, SPECIAL,  EXEMPLARY, OR CONSEQUENTIAL  DAMAGES (INCLU-
38   * DING, BUT NOT LIMITED TO, PROCUREMENT  OF SUBSTITUTE GOODS OR SERVICES; LOSS
39   * OF USE, DATA, OR  PROFITS; OR BUSINESS  INTERRUPTION)  HOWEVER CAUSED AND ON
40   * ANY  THEORY OF LIABILITY,  WHETHER  IN CONTRACT,  STRICT LIABILITY,  OR TORT
41   * (INCLUDING  NEGLIGENCE OR  OTHERWISE) ARISING IN  ANY WAY OUT OF THE  USE OF
42   * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
43   *
44   * This software  consists of voluntary contributions made  by many individuals
45   * on  behalf of the Apache Software  Foundation.  For more  information on the
46   * Apache Software Foundation, please see <http://www.apache.org/>.
47   *
48   */
49  
50  package smtphandler;
51  
52  import java.util.logging.*;
53  
54  import java.util.Date;
55  import java.util.Properties;
56  
57  import javax.mail.Authenticator;
58  import javax.mail.Message;
59  import javax.mail.Multipart;
60  import javax.mail.Session;
61  import javax.mail.Transport;
62  import javax.mail.internet.AddressException;
63  import javax.mail.internet.InternetAddress;
64  import javax.mail.internet.MimeBodyPart;
65  import javax.mail.internet.MimeMessage;
66  import javax.mail.internet.MimeMultipart;
67  
68  /**
69     Send an e-mail when a specific logging record occurs, typically on
70     {@link java.util.logging.Level#WARNING WARNING}
71     or {@link java.util.logging.Level#SEVERE SEVERE}
72  
73     <p>The number of {@link LogRecord} objects delivered in this e-mail depend on
74     the value of <b>bufferSize</b> option. The
75     <code>SMTPHandler</code> keeps only the last
76     <code>bufferSize</code> records in its cyclic buffer. This
77     keeps memory requirements at a reasonable level while still
78     delivering useful application context.</p>
79  
80     <p>
81     <img src="doc-files/smtphandler-diagram.jpg">
82     </p>
83  
84     <p>
85     The code in this class is derived from  
86     <a href="http://logging.apache.org/log4j/">log4j</a>'s 
87     SMTPAppender class.
88     </p>
89     
90     @author Ceki G&uuml;lc&uuml; (author of log4j's SMTPAppender)
91     @author Sean C. Sullivan
92  
93     */
94  public class SMTPHandler extends java.util.logging.Handler
95  {
96  	static private final int DEFAULT_ERROR_CODE = 1;
97  	static private final Formatter DEFAULT_FORMATTER = new SimpleFormatter();
98  	static private final Level DEFAULT_LEVEL = Level.WARNING;
99  	static private final int DEFAULT_BUFFER_SIZE = 512;
100 	static private final String name = SMTPHandler.class.getName();
101 	private String to;
102 	private String from;
103 	private String subject;
104 	private String smtpHost;
105 	private String smtpUsername;
106 	private String smtpPassword;
107 	private int bufferSize = DEFAULT_BUFFER_SIZE;
108 	protected CyclicBuffer cb = new CyclicBuffer(bufferSize);
109 	protected TriggeringRecordEvaluator evaluator;
110 
111 	public SMTPHandler()
112 	{
113 		final String prefix = SMTPHandler.class.getName();
114 
115 		LogManager manager = LogManager.getLogManager();
116 		 
117 		String strLevel = manager.getProperty(prefix + ".level");
118 		Level lev;
119 		if (strLevel == null)
120 		{
121 			lev = DEFAULT_LEVEL; 
122 		}
123 		try
124 		{
125 			lev = Level.parse(strLevel);
126 		}
127 		catch (Exception ex)
128 		{
129 			lev = DEFAULT_LEVEL;
130 		}
131 		setLevel(lev);
132 
133 		String strFormatterClassName = manager.getProperty(prefix + ".formatter");
134 		Formatter f = (Formatter) instantiateByClassName(strFormatterClassName, 
135 						DEFAULT_FORMATTER);
136 		this.setFormatter(f);
137 
138 
139 		setTo(manager.getProperty(prefix + ".to"));
140 		setFrom(manager.getProperty(prefix + ".from"));
141 		setSmtpHost(manager.getProperty(prefix + ".smtpHost"));
142 		setSmtpUsername(manager.getProperty(prefix + ".smtpUsername"));
143 		setSmtpPassword(manager.getProperty(prefix + ".smtpPassword"));
144 		setSubject(manager.getProperty(prefix + ".subject"));
145 
146 		String strEvaluatorClassName = manager.getProperty(prefix + ".evaluatorClass");
147 		this.setEvaluatorClass(strEvaluatorClassName);
148 		
149 		try
150 		{
151 			String strBufSize = manager.getProperty(prefix + ".bufferSize");
152 			setBufferSize(Integer.parseInt(strBufSize));
153 		}
154 		catch (Exception ex)
155 		{
156 			setBufferSize(DEFAULT_BUFFER_SIZE);
157 		}
158 
159 	}
160 
161 
162 	/**
163 	   Perform SMTPHandler specific appending actions, mainly adding
164 	   the record to a cyclic buffer and checking if the record triggers
165 	   an e-mail to be sent. */
166 	public synchronized void publish(LogRecord record)
167 	{
168 		if (record == null)
169 		{
170 			return;
171 		}
172 		else if ( ! isLoggable(record))
173 		{
174 			return;
175 		}
176 		else if (!checkEntryConditions())
177 		{
178 			return;
179 		}
180 
181 		cb.add(record);
182 
183 		if (evaluator.isTriggeringRecord(record))
184 		{
185 			sendBuffer();
186 		}
187 	}
188 
189 	/**
190 	    This method determines if there is a sense in attempting to append.
191 	
192 	    <p>It checks whether there is a "To" address set and also if
193 	    there is a set evaluator. If these checks fail, then the boolean
194 	    value <code>false</code> is returned. */
195 	protected boolean checkEntryConditions()
196 	{
197 		if (this.evaluator == null)
198 		{
199 			reportError(
200 				"No TriggeringRecordEvaluator is set for handler ["
201 					+ name
202 					+ "].",
203 				null,
204 				DEFAULT_ERROR_CODE);
205 		
206 			return false;
207 		}
208 		
209 		if (getTo() == null)
210 		{
211 			reportError(
212 				"No 'To' email address set for handler ["
213 					+ name
214 					+ "].",
215 				null,
216 				DEFAULT_ERROR_CODE);
217 			return false;
218 		
219 		}
220 
221 		return true;
222 	}
223 
224 	public synchronized void close()
225 	{
226 		// todo this.closed = true;
227 	}
228 
229 	protected InternetAddress getAddress(String addressStr)
230 	{
231 		try
232 		{
233 			return new InternetAddress(addressStr);
234 		}
235 		catch (AddressException e)
236 		{
237 			reportError("Could not parse address [" + addressStr + "].", 
238 				e, 
239 				DEFAULT_ERROR_CODE);
240 
241 			return null;
242 		}
243 	}
244 
245 	protected InternetAddress[] parseAddress(String addressStr)
246 	{
247 		try
248 		{
249 			return InternetAddress.parse(addressStr, true);
250 		}
251 		catch (AddressException e)
252 		{
253 			reportError("Could not parse address [" + addressStr + "].", 
254 					e, 
255 					DEFAULT_ERROR_CODE);
256 
257 			return new InternetAddress[0];
258 		}
259 	}
260 
261 	/**
262 	   Returns value of the <b>To</b> option.
263 	 */
264 	public String getTo()
265 	{
266 		return to;
267 	}
268 
269 	/**
270 	   Send the contents of the cyclic buffer as an e-mail message.
271 	 */
272 	protected void sendBuffer()
273 	{
274 		// Note: this code already owns the monitor for this
275 		// handler. This frees us from needing to synchronize on 'cb'.
276 
277 		Properties props = new Properties(System.getProperties());
278 
279 		if (smtpHost != null)
280 		{
281 			props.put("mail.smtp.host", smtpHost);
282 		}
283 
284 		Authenticator auth = null;
285 		
286 		if (this.getSmtpUsername() != null)
287 		{
288 			auth = new UsernamePasswordAuthenticator(
289 										this.getSmtpUsername(),
290 										this.getSmtpPassword());
291 			props.put("mail.smtp.user", this.getSmtpUsername());
292 			props.put("mail.smtp.auth", "true");
293 		}
294 		
295 		Session session = Session.getInstance(props, auth);
296 
297 		session.setDebug(true);
298 
299 		MimeMessage msg = new MimeMessage(session);
300 
301 		try
302 		{
303 			if (from != null)
304 			{
305 				msg.setFrom(getAddress(from));
306 			}
307 			else
308 			{
309 				msg.setFrom();
310 			}
311 
312 			msg.setRecipients(Message.RecipientType.TO, parseAddress(to));
313 
314 			if (subject != null)
315 			{
316 				msg.setSubject(subject);
317 			}
318 
319 			MimeBodyPart part = new MimeBodyPart();
320 
321 			StringBuffer sbuf = new StringBuffer();
322 			
323 			Formatter f = getFormatter();
324 			
325 			String head = f.getHead(this);
326 
327 			if (head != null)
328 			{
329 				sbuf.append(head);
330 			}
331 
332 			int len = cb.length();
333 
334 			for (int i = 0; i < len; i++)
335 			{
336 				LogRecord record = cb.get();
337 				sbuf.append(getFormatter().format(record));
338 			}
339 
340 			String tail = getFormatter().getTail(this);
341 
342 			if (tail != null)
343 			{
344 				sbuf.append(tail);
345 			}
346 
347 			part.setContent(sbuf.toString(), getEmailContentType());
348 
349 			Multipart mp = new MimeMultipart();
350 			mp.addBodyPart(part);
351 			msg.setContent(mp);
352 			
353 			msg.setSentDate(new Date());
354 			
355 			Transport.send(msg);
356 		}
357 		catch (Exception ex)
358 		{
359 			this.reportError("sendBuffer",
360 						ex,
361 						DEFAULT_ERROR_CODE);
362 		}
363 	}
364 
365 	protected String getEmailContentType()
366 	{
367 		return "text/plain";
368 	}
369 	
370 	/**
371 	   Returns value of the <b>EvaluatorClass</b> option.
372 	 */
373 	public String getEvaluatorClass()
374 	{
375 		return (evaluator == null) ? null : evaluator.getClass().getName();
376 	}
377 
378 	/**
379 	   Returns value of the <b>From</b> option.
380 	 */
381 	public String getFrom()
382 	{
383 		return from;
384 	}
385 
386 	/**
387 	   Returns value of the <b>Subject</b> option.
388 	 */
389 	public String getSubject()
390 	{
391 		return subject;
392 	}
393 
394 	/**
395 	   Returns value of the <b>SmtpHost</b> option.
396 	 */
397 	public String getSmtpHost()
398 	{
399 		return smtpHost;
400 	}
401 
402 	public void flush()
403 	{
404 		// todo - implement 
405 	}
406 
407 	/**
408 	   Returns value of the <b>bufferSize</b> option.
409 	 */
410 	public int getBufferSize()
411 	{
412 		return bufferSize;
413 	}
414 
415 	/**
416 	   The <b>from</b> option takes a string value which should be a
417 	   e-mail address of the sender.
418 	 */
419 	public void setFrom(String from)
420 	{
421 		this.from = from;
422 	}
423 
424 	/**
425 	   The <b>to</b> option takes a string value which should be a
426 	   e-mail address of the sender.
427 	 */
428 	public void setTo(String to)
429 	{
430 		this.to = to;
431 	}
432 
433 	/**
434 	   The <b>subject</b> option takes a string value which should be a
435 	   the subject of the e-mail message.
436 	 */
437 	public void setSubject(String subject)
438 	{
439 		this.subject = subject;
440 	}
441 
442 	/**
443 	   The <b>bufferSize</b> option takes a positive integer
444 	   representing the maximum number of logging records to collect in a
445 	   cyclic buffer. When the <code>bufferSize</code> is reached,
446 	   oldest records are deleted as new records are added to the
447 	   buffer. By default the size of the cyclic buffer is 512 records.
448 	 */
449 	public void setBufferSize(int bufferSize)
450 	{
451 		this.bufferSize = bufferSize;
452 		cb.resize(bufferSize);
453 	}
454 
455 	/**
456 	   The <b>smtpHost</b> option takes a string value which should be a
457 	   the host name of the SMTP server that will send the e-mail message.
458 	 */
459 	public void setSmtpHost(String smtpHost)
460 	{
461 		this.smtpHost = smtpHost;
462 	}
463 
464 	/**
465 	   The <b>evaluatorClass</b> option takes a string value
466 	   representing the name of the class implementing the {@link
467 	   TriggeringRecordEvaluator} interface. A corresponding object will
468 	   be instantiated and assigned as the triggering record evaluator
469 	   for the SMTPHandler.
470 	 */
471 	public void setEvaluatorClass(String value)
472 	{
473 		evaluator =
474 		(TriggeringRecordEvaluator) instantiateByClassName(value, new DefaultEvaluator());
475 	}
476 
477 	static Object instantiateByClassName(String strClassName, Object defaultObj)
478 	{
479 		Object result;
480 		
481 		try
482 		{
483 			ClassLoader loader = Thread.currentThread().getContextClassLoader();
484 			Class clazz = loader.loadClass(strClassName);
485 			result = clazz.newInstance();
486 		}
487 		catch (Exception ex)
488 		{
489 			result = defaultObj;
490 		}
491 		return result;
492 
493 	}
494 	public String toString()
495 	{
496 		StringBuffer sb = new StringBuffer();
497 		sb.append("SmtpHost=");
498 		sb.append(String.valueOf(this.getSmtpHost()));
499 		sb.append("\nFrom=");
500 		sb.append(String.valueOf(this.getFrom()));
501 		sb.append("\nTo=");
502 		sb.append(String.valueOf(this.getTo()));
503 		sb.append("\nSubject=");
504 		sb.append(String.valueOf(this.getSubject()));
505 		sb.append("\nFormatter=");
506 		sb.append(String.valueOf(this.getFormatter()));
507 		sb.append("\nLevel=");
508 		sb.append(String.valueOf(this.getLevel()));
509 		sb.append("\nBufferSize=");
510 		sb.append(String.valueOf(this.getBufferSize()));
511 		return sb.toString();
512 	}
513 
514 
515 	public String getSmtpPassword()
516 	{
517 		return smtpPassword;
518 	}
519 
520 
521 	public void setSmtpPassword(String value)
522 	{
523 		this.smtpPassword = value;
524 	}
525 
526 
527 	public String getSmtpUsername()
528 	{
529 		return smtpUsername;
530 	}
531 
532 
533 	public void setSmtpUsername(String value)
534 	{
535 		this.smtpUsername = value;
536 	}
537 
538 }
539 
540 class DefaultEvaluator implements TriggeringRecordEvaluator
541 {
542 	/**
543 	
544 	   Should this <code>record</code> trigger an email message to 
545 	   be sent?
546 	
547 	   <p>This method returns <code>true</code>, if the record level
548 	   has WARNING level or higher. Otherwise it returns
549 	   <code>false</code>.
550 	    
551 	   */
552 	public boolean isTriggeringRecord(final LogRecord record)
553 	{
554 		boolean result = false;
555 		
556 		if (record == null)
557 		{
558 			result = false;
559 		}
560 		else if (record.getLevel() == null)
561 		{
562 			result = false;
563 		}
564 		else
565 		{
566 			result = record.getLevel().intValue() >= java.util.logging.Level.WARNING.intValue();
567 		}
568 		
569 		return result;
570 	}
571 	
572 
573 }