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