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