001/**
002 * Logback: the reliable, generic, fast and flexible logging framework.
003 * Copyright (C) 1999-2015, QOS.ch. All rights reserved.
004 *
005 * This program and the accompanying materials are dual-licensed under
006 * either the terms of the Eclipse Public License v1.0 as published by
007 * the Eclipse Foundation
008 *
009 *   or (per the licensee's choosing)
010 *
011 * under the terms of the GNU Lesser General Public License version 2.1
012 * as published by the Free Software Foundation.
013 */
014package ch.qos.logback.core.net;
015
016import java.util.ArrayList;
017import java.util.Arrays;
018import java.util.Date;
019import java.util.List;
020import java.util.Properties;
021
022import javax.mail.Message;
023import javax.mail.Multipart;
024import javax.mail.Session;
025import javax.mail.Transport;
026import javax.mail.internet.AddressException;
027import javax.mail.internet.InternetAddress;
028import javax.mail.internet.MimeBodyPart;
029import javax.mail.internet.MimeMessage;
030import javax.mail.internet.MimeMultipart;
031import javax.naming.Context;
032import javax.naming.InitialContext;
033
034import ch.qos.logback.core.AppenderBase;
035import ch.qos.logback.core.CoreConstants;
036import ch.qos.logback.core.Layout;
037import ch.qos.logback.core.boolex.EvaluationException;
038import ch.qos.logback.core.boolex.EventEvaluator;
039import ch.qos.logback.core.helpers.CyclicBuffer;
040import ch.qos.logback.core.pattern.PatternLayoutBase;
041import ch.qos.logback.core.sift.DefaultDiscriminator;
042import ch.qos.logback.core.sift.Discriminator;
043import ch.qos.logback.core.spi.CyclicBufferTracker;
044import ch.qos.logback.core.util.ContentTypeUtil;
045import ch.qos.logback.core.util.OptionHelper;
046
047// Contributors:
048// Andrey Rybin charset encoding support http://jira.qos.ch/browse/LBCORE-69
049
050/**
051 * An abstract class that provides support for sending events to an email
052 * address.
053 * <p>
054 * See http://logback.qos.ch/manual/appenders.html#SMTPAppender for further
055 * documentation.
056 *
057 * @author Ceki G&uuml;lc&uuml;
058 * @author S&eacute;bastien Pennec
059 */
060public abstract class SMTPAppenderBase<E> extends AppenderBase<E> {
061
062    static InternetAddress[] EMPTY_IA_ARRAY = new InternetAddress[0];
063    // ~ 14 days
064    static final long MAX_DELAY_BETWEEN_STATUS_MESSAGES = 1228800 * CoreConstants.MILLIS_IN_ONE_SECOND;
065
066    long lastTrackerStatusPrint = 0;
067    long delayBetweenStatusMessages = 300 * CoreConstants.MILLIS_IN_ONE_SECOND;
068
069    protected Layout<E> subjectLayout;
070    protected Layout<E> layout;
071
072    private List<PatternLayoutBase<E>> toPatternLayoutList = new ArrayList<PatternLayoutBase<E>>();
073    private String from;
074    private String subjectStr = null;
075    private String smtpHost;
076    private int smtpPort = 25;
077    private boolean starttls = false;
078    private boolean ssl = false;
079    private boolean sessionViaJNDI = false;
080    private String jndiLocation = CoreConstants.JNDI_COMP_PREFIX + "/mail/Session";
081
082    String username;
083    String password;
084    String localhost;
085
086    boolean asynchronousSending = true;
087
088    private String charsetEncoding = "UTF-8";
089
090    protected Session session;
091
092    protected EventEvaluator<E> eventEvaluator;
093
094    protected Discriminator<E> discriminator = new DefaultDiscriminator<E>();
095    protected CyclicBufferTracker<E> cbTracker;
096
097    private int errorCount = 0;
098
099    /**
100     * return a layout for the subject string as appropriate for the module. If the
101     * subjectStr parameter is null, then a default value for subjectStr should be
102     * used.
103     *
104     * @param subjectStr
105     * @return a layout as appropriate for the module
106     */
107    abstract protected Layout<E> makeSubjectLayout(String subjectStr);
108
109    /**
110     * Start the appender
111     */
112    public void start() {
113
114        if (cbTracker == null) {
115            cbTracker = new CyclicBufferTracker<E>();
116        }
117
118        if (sessionViaJNDI)
119            session = lookupSessionInJNDI();
120        else
121            session = buildSessionFromProperties();
122
123        if (session == null) {
124            addError("Failed to obtain javax.mail.Session. Cannot start.");
125            return;
126        }
127
128        subjectLayout = makeSubjectLayout(subjectStr);
129
130        started = true;
131    }
132
133    private Session lookupSessionInJNDI() {
134        addInfo("Looking up javax.mail.Session at JNDI location [" + jndiLocation + "]");
135        try {
136            Context initialContext = new InitialContext();
137            Object obj = initialContext.lookup(jndiLocation);
138            return (Session) obj;
139        } catch (Exception e) {
140            addError("Failed to obtain javax.mail.Session from JNDI location [" + jndiLocation + "]");
141            return null;
142        }
143    }
144
145    private Session buildSessionFromProperties() {
146        Properties props = new Properties(OptionHelper.getSystemProperties());
147        if (smtpHost != null) {
148            props.put("mail.smtp.host", smtpHost);
149        }
150        props.put("mail.smtp.port", Integer.toString(smtpPort));
151
152        if (localhost != null) {
153            props.put("mail.smtp.localhost", localhost);
154        }
155
156        LoginAuthenticator loginAuthenticator = null;
157
158        if (username != null) {
159            loginAuthenticator = new LoginAuthenticator(username, password);
160            props.put("mail.smtp.auth", "true");
161        }
162
163        if (isSTARTTLS() && isSSL()) {
164            addError("Both SSL and StartTLS cannot be enabled simultaneously");
165        } else {
166            if (isSTARTTLS()) {
167                // see also http://jira.qos.ch/browse/LOGBACK-193
168                props.put("mail.smtp.starttls.enable", "true");
169                props.put("mail.transport.protocol", "true");
170            }
171            if (isSSL()) {
172                props.put("mail.smtp.ssl.enable", "true");
173            }
174        }
175
176        // props.put("mail.debug", "true");
177
178        return Session.getInstance(props, loginAuthenticator);
179    }
180
181    /**
182     * Perform SMTPAppender specific appending actions, delegating some of them to
183     * a subclass and checking if the event triggers an e-mail to be sent.
184     */
185    protected void append(E eventObject) {
186
187        if (!checkEntryConditions()) {
188            return;
189        }
190
191        String key = discriminator.getDiscriminatingValue(eventObject);
192        long now = System.currentTimeMillis();
193        final CyclicBuffer<E> cb = cbTracker.getOrCreate(key, now);
194        subAppend(cb, eventObject);
195
196        try {
197            if (eventEvaluator.evaluate(eventObject)) {
198                // clone the CyclicBuffer before sending out asynchronously
199                CyclicBuffer<E> cbClone = new CyclicBuffer<E>(cb);
200                // see http://jira.qos.ch/browse/LBCLASSIC-221
201                cb.clear();
202
203                if (asynchronousSending) {
204                    // perform actual sending asynchronously
205                    SenderRunnable senderRunnable = new SenderRunnable(cbClone, eventObject);
206                    context.getScheduledExecutorService().execute(senderRunnable);
207                } else {
208                    // synchronous sending
209                    sendBuffer(cbClone, eventObject);
210                }
211            }
212        } catch (EvaluationException ex) {
213            errorCount++;
214            if (errorCount < CoreConstants.MAX_ERROR_COUNT) {
215                addError("SMTPAppender's EventEvaluator threw an Exception-", ex);
216            }
217        }
218
219        // immediately remove the buffer if asked by the user
220        if (eventMarksEndOfLife(eventObject)) {
221            cbTracker.endOfLife(key);
222        }
223
224        cbTracker.removeStaleComponents(now);
225
226        if (lastTrackerStatusPrint + delayBetweenStatusMessages < now) {
227            addInfo("SMTPAppender [" + name + "] is tracking [" + cbTracker.getComponentCount() + "] buffers");
228            lastTrackerStatusPrint = now;
229            // quadruple 'delay' assuming less than max delay
230            if (delayBetweenStatusMessages < MAX_DELAY_BETWEEN_STATUS_MESSAGES) {
231                delayBetweenStatusMessages *= 4;
232            }
233        }
234    }
235
236    abstract protected boolean eventMarksEndOfLife(E eventObject);
237
238    abstract protected void subAppend(CyclicBuffer<E> cb, E eventObject);
239
240    /**
241     * This method determines if there is a sense in attempting to append.
242     * <p>
243     * It checks whether there is a set output target and also if there is a set
244     * layout. If these checks fail, then the boolean value <code>false</code> is
245     * returned.
246     */
247    public boolean checkEntryConditions() {
248        if (!this.started) {
249            addError("Attempting to append to a non-started appender: " + this.getName());
250            return false;
251        }
252
253        if (this.eventEvaluator == null) {
254            addError("No EventEvaluator is set for appender [" + name + "].");
255            return false;
256        }
257
258        if (this.layout == null) {
259            addError("No layout set for appender named [" + name + "]. For more information, please visit http://logback.qos.ch/codes.html#smtp_no_layout");
260            return false;
261        }
262        return true;
263    }
264
265    synchronized public void stop() {
266        this.started = false;
267    }
268
269    InternetAddress getAddress(String addressStr) {
270        try {
271            return new InternetAddress(addressStr);
272        } catch (AddressException e) {
273            addError("Could not parse address [" + addressStr + "].", e);
274            return null;
275        }
276    }
277
278    private List<InternetAddress> parseAddress(E event) {
279        int len = toPatternLayoutList.size();
280
281        List<InternetAddress> iaList = new ArrayList<InternetAddress>();
282
283        for (int i = 0; i < len; i++) {
284            try {
285                PatternLayoutBase<E> emailPL = toPatternLayoutList.get(i);
286                String emailAdrr = emailPL.doLayout(event);
287                if (emailAdrr == null || emailAdrr.length() == 0) {
288                    continue;
289                }
290                InternetAddress[] tmp = InternetAddress.parse(emailAdrr, true);
291                iaList.addAll(Arrays.asList(tmp));
292            } catch (AddressException e) {
293                addError("Could not parse email address for [" + toPatternLayoutList.get(i) + "] for event [" + event + "]", e);
294                return iaList;
295            }
296        }
297
298        return iaList;
299    }
300
301    /**
302     * Returns value of the <b>toList</b> option.
303     */
304    public List<PatternLayoutBase<E>> getToList() {
305        return toPatternLayoutList;
306    }
307
308    /**
309     * Allows extend classes to update mime message (e.g.: Add headers)
310     */
311    protected void updateMimeMsg(MimeMessage mimeMsg, CyclicBuffer<E> cb, E lastEventObject) {
312    }
313
314    /**
315     * Send the contents of the cyclic buffer as an e-mail message.
316     */
317    protected void sendBuffer(CyclicBuffer<E> cb, E lastEventObject) {
318
319        // Note: this code already owns the monitor for this
320        // appender. This frees us from needing to synchronize on 'cb'.
321        try {
322            MimeBodyPart part = new MimeBodyPart();
323
324            StringBuffer sbuf = new StringBuffer();
325
326            String header = layout.getFileHeader();
327            if (header != null) {
328                sbuf.append(header);
329            }
330            String presentationHeader = layout.getPresentationHeader();
331            if (presentationHeader != null) {
332                sbuf.append(presentationHeader);
333            }
334            fillBuffer(cb, sbuf);
335            String presentationFooter = layout.getPresentationFooter();
336            if (presentationFooter != null) {
337                sbuf.append(presentationFooter);
338            }
339            String footer = layout.getFileFooter();
340            if (footer != null) {
341                sbuf.append(footer);
342            }
343
344            String subjectStr = "Undefined subject";
345            if (subjectLayout != null) {
346                subjectStr = subjectLayout.doLayout(lastEventObject);
347
348                // The subject must not contain new-line characters, which cause
349                // an SMTP error (LOGBACK-865). Truncate the string at the first
350                // new-line character.
351                int newLinePos = (subjectStr != null) ? subjectStr.indexOf('\n') : -1;
352                if (newLinePos > -1) {
353                    subjectStr = subjectStr.substring(0, newLinePos);
354                }
355            }
356
357            MimeMessage mimeMsg = new MimeMessage(session);
358
359            if (from != null) {
360                mimeMsg.setFrom(getAddress(from));
361            } else {
362                mimeMsg.setFrom();
363            }
364
365            mimeMsg.setSubject(subjectStr, charsetEncoding);
366
367            List<InternetAddress> destinationAddresses = parseAddress(lastEventObject);
368            if (destinationAddresses.isEmpty()) {
369                addInfo("Empty destination address. Aborting email transmission");
370                return;
371            }
372
373            InternetAddress[] toAddressArray = destinationAddresses.toArray(EMPTY_IA_ARRAY);
374            mimeMsg.setRecipients(Message.RecipientType.TO, toAddressArray);
375
376            String contentType = layout.getContentType();
377
378            if (ContentTypeUtil.isTextual(contentType)) {
379                part.setText(sbuf.toString(), charsetEncoding, ContentTypeUtil.getSubType(contentType));
380            } else {
381                part.setContent(sbuf.toString(), layout.getContentType());
382            }
383
384            Multipart mp = new MimeMultipart();
385            mp.addBodyPart(part);
386            mimeMsg.setContent(mp);
387
388            // Added the feature to update mime message before sending the email
389            updateMimeMsg(mimeMsg, cb, lastEventObject);
390
391            mimeMsg.setSentDate(new Date());
392            addInfo("About to send out SMTP message \"" + subjectStr + "\" to " + Arrays.toString(toAddressArray));
393            Transport.send(mimeMsg);
394        } catch (Exception e) {
395            addError("Error occurred while sending e-mail notification.", e);
396        }
397    }
398
399    abstract protected void fillBuffer(CyclicBuffer<E> cb, StringBuffer sbuf);
400
401    /**
402     * Returns value of the <b>From</b> option.
403     */
404    public String getFrom() {
405        return from;
406    }
407
408    /**
409     * Returns value of the <b>Subject</b> option.
410     */
411    public String getSubject() {
412        return subjectStr;
413    }
414
415    /**
416     * The <b>From</b> option takes a string value which should be a e-mail
417     * address of the sender.
418     */
419    public void setFrom(String from) {
420        this.from = from;
421    }
422
423    /**
424     * The <b>Subject</b> option takes a string value which should be a the
425     * subject of the e-mail message.
426     */
427    public void setSubject(String subject) {
428        this.subjectStr = subject;
429    }
430
431    /**
432     * Alias for smtpHost
433     *
434     * @param smtpHost
435     */
436    public void setSMTPHost(String smtpHost) {
437        setSmtpHost(smtpHost);
438    }
439
440    /**
441     * The <b>smtpHost</b> option takes a string value which should be a the host
442     * name of the SMTP server that will send the e-mail message.
443     */
444    public void setSmtpHost(String smtpHost) {
445        this.smtpHost = smtpHost;
446    }
447
448    /**
449     * Alias for getSmtpHost().
450     */
451    public String getSMTPHost() {
452        return getSmtpHost();
453    }
454
455    /**
456     * Returns value of the <b>SMTPHost</b> option.
457     */
458    public String getSmtpHost() {
459        return smtpHost;
460    }
461
462    /**
463     * Alias for {@link #setSmtpPort}.
464     *
465     * @param port
466     */
467    public void setSMTPPort(int port) {
468        setSmtpPort(port);
469    }
470
471    /**
472     * The port where the SMTP server is running. Default value is 25.
473     *
474     * @param port
475     */
476    public void setSmtpPort(int port) {
477        this.smtpPort = port;
478    }
479
480    /**
481     * Alias for {@link #getSmtpPort}
482     *
483     * @return
484     */
485    public int getSMTPPort() {
486        return getSmtpPort();
487    }
488
489    /**
490     * See {@link #setSmtpPort}
491     *
492     * @return
493     */
494    public int getSmtpPort() {
495        return smtpPort;
496    }
497
498    public String getLocalhost() {
499        return localhost;
500    }
501
502    /**
503     * Set the "mail.smtp.localhost" property to the value passed as parameter to
504     * this method.
505     * 
506     * <p>Useful in case the hostname for the client host is not fully qualified
507     * and as a consequence the SMTP server rejects the clients HELO/EHLO command.
508     * </p>
509     *
510     * @param localhost
511     */
512    public void setLocalhost(String localhost) {
513        this.localhost = localhost;
514    }
515
516    public CyclicBufferTracker<E> getCyclicBufferTracker() {
517        return cbTracker;
518    }
519
520    public void setCyclicBufferTracker(CyclicBufferTracker<E> cbTracker) {
521        this.cbTracker = cbTracker;
522    }
523
524    public Discriminator<E> getDiscriminator() {
525        return discriminator;
526    }
527
528    public void setDiscriminator(Discriminator<E> discriminator) {
529        this.discriminator = discriminator;
530    }
531
532    public boolean isAsynchronousSending() {
533        return asynchronousSending;
534    }
535
536    /**
537     * By default, SMTAppender transmits emails asynchronously. For synchronous email transmission set
538     * asynchronousSending to 'false'.
539     *
540     * @param asynchronousSending determines whether sending is done asynchronously or not
541     * @since 1.0.4
542     */
543    public void setAsynchronousSending(boolean asynchronousSending) {
544        this.asynchronousSending = asynchronousSending;
545    }
546
547    public void addTo(String to) {
548        if (to == null || to.length() == 0) {
549            throw new IllegalArgumentException("Null or empty <to> property");
550        }
551        PatternLayoutBase<E> plb = makeNewToPatternLayout(to.trim());
552        plb.setContext(context);
553        plb.start();
554        this.toPatternLayoutList.add(plb);
555    }
556
557    abstract protected PatternLayoutBase<E> makeNewToPatternLayout(String toPattern);
558
559    public List<String> getToAsListOfString() {
560        List<String> toList = new ArrayList<String>();
561        for (PatternLayoutBase<E> plb : toPatternLayoutList) {
562            toList.add(plb.getPattern());
563        }
564        return toList;
565    }
566
567    public boolean isSTARTTLS() {
568        return starttls;
569    }
570
571    public void setSTARTTLS(boolean startTLS) {
572        this.starttls = startTLS;
573    }
574
575    public boolean isSSL() {
576        return ssl;
577    }
578
579    public void setSSL(boolean ssl) {
580        this.ssl = ssl;
581    }
582
583    /**
584     * The <b>EventEvaluator</b> option takes a string value representing the name
585     * of the class implementing the {@link EventEvaluator} interface. A
586     * corresponding object will be instantiated and assigned as the event
587     * evaluator for the SMTPAppender.
588     */
589    public void setEvaluator(EventEvaluator<E> eventEvaluator) {
590        this.eventEvaluator = eventEvaluator;
591    }
592
593    public String getUsername() {
594        return username;
595    }
596
597    public void setUsername(String username) {
598        this.username = username;
599    }
600
601    public String getPassword() {
602        return password;
603    }
604
605    public void setPassword(String password) {
606        this.password = password;
607    }
608
609    /**
610     * @return the charset encoding value
611     * @see #setCharsetEncoding(String)
612     */
613    public String getCharsetEncoding() {
614        return charsetEncoding;
615    }
616
617    public String getJndiLocation() {
618        return jndiLocation;
619    }
620
621    /**
622     * Set the location where a {@link javax.mail.Session} resource is located in JNDI. Default value is
623     * "java:comp/env/mail/Session".
624     *
625     * @param jndiLocation
626     * @since 1.0.6
627     */
628    public void setJndiLocation(String jndiLocation) {
629        this.jndiLocation = jndiLocation;
630    }
631
632    public boolean isSessionViaJNDI() {
633        return sessionViaJNDI;
634    }
635
636    /**
637     * If set to true, a {@link javax.mail.Session} resource will be retrieved from JNDI. Default is false.
638     *
639     * @param sessionViaJNDI whether to obtain a javax.mail.Session by JNDI
640     * @since 1.0.6
641     */
642    public void setSessionViaJNDI(boolean sessionViaJNDI) {
643        this.sessionViaJNDI = sessionViaJNDI;
644    }
645
646    /**
647     * Set the character set encoding of the outgoing email messages. The default
648     * encoding is "UTF-8" which usually works well for most purposes.
649     *
650     * @param charsetEncoding
651     */
652    public void setCharsetEncoding(String charsetEncoding) {
653        this.charsetEncoding = charsetEncoding;
654    }
655
656    public Layout<E> getLayout() {
657        return layout;
658    }
659
660    public void setLayout(Layout<E> layout) {
661        this.layout = layout;
662    }
663
664    class SenderRunnable implements Runnable {
665
666        final CyclicBuffer<E> cyclicBuffer;
667        final E e;
668
669        SenderRunnable(CyclicBuffer<E> cyclicBuffer, E e) {
670            this.cyclicBuffer = cyclicBuffer;
671            this.e = e;
672        }
673
674        public void run() {
675            sendBuffer(cyclicBuffer, e);
676        }
677    }
678}