SMTPAppender_GreenTest.java

/*
 * Logback: the reliable, generic, fast and flexible logging framework.
 * Copyright (C) 1999-2026, QOS.ch. All rights reserved.
 *
 * This program and the accompanying materials are dual-licensed under
 * either the terms of the Eclipse Public License v2.0 as published by
 * the Eclipse Foundation
 *
 *   or (per the licensee's choosing)
 *
 * under the terms of the GNU Lesser General Public License version 2.1
 * as published by the Free Software Foundation.
 */
package ch.qos.logback.classic.blackbox.net;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.blackbox.BlackboxClassicTestConstants;
import ch.qos.logback.classic.net.SMTPAppender;
import ch.qos.logback.classic.util.LogbackMDCAdapter;
import ch.qos.logback.core.util.EnvUtil;
import com.icegreen.greenmail.util.*;
import org.dom4j.DocumentException;
import org.dom4j.io.SAXReader;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

//import ch.qos.logback.classic.ClassicTestConstants;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.PatternLayout;
import ch.qos.logback.classic.html.HTMLLayout;
import ch.qos.logback.classic.blackbox.html.XHTMLEntityResolver;
import ch.qos.logback.classic.joran.JoranConfigurator;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.Layout;
import ch.qos.logback.core.joran.spi.JoranException;
import ch.qos.logback.core.status.OnConsoleStatusListener;
import ch.qos.logback.core.testUtil.RandomUtil;
import ch.qos.logback.core.util.StatusListenerConfigHelper;
import ch.qos.logback.core.util.StatusPrinter;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import jakarta.mail.internet.MimeMultipart;

import static org.junit.jupiter.api.Assertions.*;

public class SMTPAppender_GreenTest {

    static boolean NO_SSL = false;
    static boolean WITH_SSL = true;

    static final String HEADER = "HEADER\n";
    static final String FOOTER = "FOOTER\n";
    static final String DEFAULT_PATTERN = "%-4relative %mdc [%thread] %-5level %class - %msg%n";

    static final String JAVAX_NET_DEBUG_KEY = "javax.net.debug";
    static final String SSH_HANDSHAKE_DEBUG_VAL =  "ssl:handshake";
    // for use in authenticated SSL tests only (to avoid SSL handshake failures)
    static final String CHECK_SERVER_IDENTITY_KEY = "mail.smtp.ssl.checkserveridentity";

    static final boolean SYNCHRONOUS = false;
    static final boolean ASYNCHRONOUS = true;
    static int TIMEOUT = 3000;

    int port = RandomUtil.getRandomServerPort();
    // GreenMail cannot be static. As a shared server induces race conditions
    GreenMail greenMailServer;

    SMTPAppender smtpAppender;
    LoggerContext loggerContext = new LoggerContext();
    LogbackMDCAdapter logbackMDCAdapter = new LogbackMDCAdapter();
    Logger logger = loggerContext.getLogger(this.getClass());

    static String REQUIRED_USERNAME = "alice";
    static String REQUIRED_PASSWORD = "alicepass";


    @BeforeEach
    public void setUp() throws Exception {
        loggerContext.setMDCAdapter(logbackMDCAdapter);
        StatusListenerConfigHelper.addOnConsoleListenerInstance(loggerContext, new OnConsoleStatusListener());
        setGlobalLogbackLoggerForGreenmail(Level.INFO);
    }


    @AfterEach
    public void tearDown() throws Exception {
        greenMailServer.stop();
        setGlobalLogbackLoggerForGreenmail(null);
    }

    private static void setGlobalLogbackLoggerForGreenmail(Level level) {
        Logger logbackLogger = (Logger) LoggerFactory.getLogger("com.icegreen.greenmail");
        logbackLogger.setLevel(level);
    }


    void startSMTPServer(boolean withSSL) {
        ServerSetup serverSetup;

        if (withSSL) {
            serverSetup = new ServerSetup(port, null, ServerSetup.PROTOCOL_SMTPS);
        } else {
            serverSetup = new ServerSetup(port, null, ServerSetup.PROTOCOL_SMTP);
        }
        greenMailServer = new GreenMail(serverSetup);
        // user password is checked for the specified user ONLY
        greenMailServer.setUser(REQUIRED_USERNAME, REQUIRED_PASSWORD);
        greenMailServer.start();
        // give the server a head start
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
        }
    }


    void buildSMTPAppender(String subject, boolean synchronicity) throws Exception {
        smtpAppender = new SMTPAppender();
        smtpAppender.setContext(loggerContext);
        smtpAppender.setName("smtp");
        smtpAppender.setFrom("user@host.dom");
        smtpAppender.setSMTPHost("localhost");
        smtpAppender.setSMTPPort(port);
        smtpAppender.setSubject(subject);
        smtpAppender.addTo("nospam@qos.ch");
        smtpAppender.setAsynchronousSending(synchronicity);
    }

    private Layout<ILoggingEvent> buildPatternLayout(String pattern) {
        PatternLayout layout = new PatternLayout();
        layout.setContext(loggerContext);
        layout.setFileHeader(HEADER);
        layout.setOutputPatternAsHeader(false);
        layout.setPattern(pattern);
        layout.setFileFooter(FOOTER);
        layout.start();
        return layout;
    }

    private Layout<ILoggingEvent> buildHTMLLayout() {
        HTMLLayout layout = new HTMLLayout();
        layout.setContext(loggerContext);
        layout.setPattern("%level%class%msg");
        layout.start();
        return layout;
    }

    private void waitForServerToReceiveEmails(int emailCount) throws InterruptedException {
        greenMailServer.waitForIncomingEmail(5000, emailCount);
    }

    private MimeMultipart verifyAndExtractMimeMultipart(String subject)
            throws MessagingException, IOException, InterruptedException {
        int oldCount = 0;
        int expectedEmailCount = 1;
        // wait for the server to receive the messages
        waitForServerToReceiveEmails(expectedEmailCount);
        MimeMessage[] mma = greenMailServer.getReceivedMessages();
        assertNotNull(mma);
        assertEquals(expectedEmailCount, mma.length);
        MimeMessage mm = mma[oldCount];
        // http://jira.qos.ch/browse/LBCLASSIC-67
        assertEquals(subject, mm.getSubject());
        return (MimeMultipart) mm.getContent();
    }



    void waitUntilEmailIsSent() throws InterruptedException {
        ExecutorService es = loggerContext.getExecutorService();
        es.shutdown();
        boolean terminated = es.awaitTermination(TIMEOUT, TimeUnit.MILLISECONDS);

        // this assertion may be needlessly strict, skipped on MacOS
        if(!terminated && !EnvUtil.isMacOs()) {
            fail("executor elapsed before accorded delay " + System.getProperty("os.name"));
        }

    }

    @Test
    public void synchronousSmoke() throws Exception {
        startSMTPServer(NO_SSL);
        String subject = "synchronousSmoke";
        buildSMTPAppender(subject, SYNCHRONOUS);

        smtpAppender.setLayout(buildPatternLayout(DEFAULT_PATTERN));

        smtpAppender.start();

        logger.addAppender(smtpAppender);
        logger.debug("hello");
        logger.error("en error", new Exception("an exception"));

        MimeMultipart mp = verifyAndExtractMimeMultipart(subject);
        String body = GreenMailUtil.getBody(mp.getBodyPart(0));
        assertTrue(body.startsWith(HEADER.trim()));
        assertTrue(body.endsWith(FOOTER.trim()));
    }

    @Test
    public void asynchronousSmoke() throws Exception {
        startSMTPServer(NO_SSL);

        String subject = "asynchronousSmoke";
        buildSMTPAppender(subject, ASYNCHRONOUS);
        smtpAppender.setLayout(buildPatternLayout(DEFAULT_PATTERN));
        smtpAppender.start();

        logger.addAppender(smtpAppender);
        logger.debug("hello");
        logger.error("en error", new Exception("an exception"));

        waitUntilEmailIsSent();
        MimeMultipart mp = verifyAndExtractMimeMultipart(subject);
        String body = GreenMailUtil.getBody(mp.getBodyPart(0));
        assertTrue(body.startsWith(HEADER.trim()));
        assertTrue(body.endsWith(FOOTER.trim()));
    }

    // See also http://jira.qos.ch/browse/LOGBACK-734
    @Test
    public void callerDataShouldBeCorrectlySetWithAsynchronousSending() throws Exception {
        startSMTPServer(NO_SSL);
        String subject = "LOGBACK-734";
        buildSMTPAppender("LOGBACK-734", ASYNCHRONOUS);
        smtpAppender.setLayout(buildPatternLayout(DEFAULT_PATTERN));
        smtpAppender.setIncludeCallerData(true);
        smtpAppender.start();
        logger.addAppender(smtpAppender);
        logger.debug("LOGBACK-734");
        logger.error("callerData", new Exception("ShouldBeCorrectlySetWithAsynchronousSending"));

        waitUntilEmailIsSent();
        MimeMultipart mp = verifyAndExtractMimeMultipart(subject);
        String body = GreenMailUtil.getBody(mp.getBodyPart(0));
        assertTrue(body.contains("DEBUG " + this.getClass().getName() + " - LOGBACK-734"), "actual [" + body + "]");
    }

    // lost MDC
    @Test
    public void LOGBACK_352() throws Exception {
        startSMTPServer(NO_SSL);
        String subject = "LOGBACK_352";
        buildSMTPAppender(subject, SYNCHRONOUS);
        smtpAppender.setAsynchronousSending(false);
        smtpAppender.setLayout(buildPatternLayout(DEFAULT_PATTERN));
        smtpAppender.start();
        logger.addAppender(smtpAppender);
        logbackMDCAdapter.put("key", "val");
        logger.debug("LBCLASSIC_104");
        logbackMDCAdapter.clear();
        logger.error("en error", new Exception("test"));

        MimeMultipart mp = verifyAndExtractMimeMultipart(subject);
        String body = GreenMailUtil.getBody(mp.getBodyPart(0));
        assertTrue(body.startsWith(HEADER.trim()));
        System.out.println(body);
        assertTrue(body.contains("key=val"));
        assertTrue(body.endsWith(FOOTER.trim()));
    }

    @Test
    public void html() throws Exception {
        startSMTPServer(NO_SSL);
        String subject = "html";
        buildSMTPAppender(subject, SYNCHRONOUS);
        smtpAppender.setAsynchronousSending(false);
        smtpAppender.setLayout(buildHTMLLayout());
        smtpAppender.start();
        logger.addAppender(smtpAppender);
        logger.debug("html");
        logger.error("en error", new Exception("an exception"));

        MimeMultipart mp = verifyAndExtractMimeMultipart(subject);

        // verifyAndExtractMimeMultipart strict adherence to xhtml1-strict.dtd
        SAXReader reader = new SAXReader();
        reader.setValidation(true);
        reader.setEntityResolver(new XHTMLEntityResolver());
        byte[] messageBytes = getAsByteArray(mp.getBodyPart(0).getInputStream());
        ByteArrayInputStream bais = new ByteArrayInputStream(messageBytes);
        try {
            reader.read(bais);
        } catch (DocumentException de) {
            System.out.println("incoming message:");
            System.out.println(new String(messageBytes));
            throw de;
        }
        System.out.println("incoming message:");
        System.out.println(new String(messageBytes));
    }

    private byte[] getAsByteArray(InputStream inputStream) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        byte[] buffer = new byte[1024];
        int n = -1;
        while ((n = inputStream.read(buffer)) != -1) {
            baos.write(buffer, 0, n);
        }
        return baos.toByteArray();
    }

    private void configure(String file) throws JoranException {
        JoranConfigurator jc = new JoranConfigurator();
        jc.setContext(loggerContext);
        loggerContext.putProperty("port", "" + port);
        jc.doConfigure(file);
    }

    @Test
    public void testCustomEvaluator() throws Exception {
        startSMTPServer(NO_SSL);
        configure(BlackboxClassicTestConstants.JORAN_INPUT_PREFIX + "smtp/customEvaluator.xml");

        logger.debug("test");
        String msg2 = "CustomEvaluator";
        logger.debug(msg2);
        logger.debug("invisible");
        waitUntilEmailIsSent();
        MimeMultipart mp = verifyAndExtractMimeMultipart(
                "testCustomEvaluator " + this.getClass().getName() + " - " + msg2);
        String body = GreenMailUtil.getBody(mp.getBodyPart(0));
        assertEquals("testCustomEvaluator", body);
    }

    @Test
    public void testCustomBufferSize() throws Exception {
        startSMTPServer(NO_SSL);
        configure(BlackboxClassicTestConstants.JORAN_INPUT_PREFIX + "smtp/customBufferSize.xml");

        logger.debug("invisible1");
        logger.debug("invisible2");
        String msg = "hello";
        logger.error(msg);
        waitUntilEmailIsSent();
        MimeMultipart mp = verifyAndExtractMimeMultipart(
                "testCustomBufferSize " + this.getClass().getName() + " - " + msg);
        String body = GreenMailUtil.getBody(mp.getBodyPart(0));
        assertEquals(msg, body);
    }

    // this test fails intermittently on Jenkins.
    @Test
    public void testMultipleTo() throws Exception {
        startSMTPServer(NO_SSL);
        buildSMTPAppender("testMultipleTo", SYNCHRONOUS);
        smtpAppender.setLayout(buildPatternLayout(DEFAULT_PATTERN));
        // buildSMTPAppender() already added one destination address
        smtpAppender.addTo("Test <test@example.com>, other-test@example.com");
        smtpAppender.start();
        logger.addAppender(smtpAppender);
        logger.debug("testMultipleTo hello");
        logger.error("testMultipleTo en error", new Exception("an exception"));
        Thread.yield();
        int expectedEmailCount = 3;
        waitForServerToReceiveEmails(expectedEmailCount);
        MimeMessage[] mma = greenMailServer.getReceivedMessages();
        assertNotNull(mma);
        assertEquals(expectedEmailCount, mma.length);
    }

    // http://jira.qos.ch/browse/LBCLASSIC-221
    @Test
    public void bufferShouldBeResetBetweenMessages() throws Exception {
        startSMTPServer(NO_SSL);
        buildSMTPAppender("bufferShouldBeResetBetweenMessages", SYNCHRONOUS);
        smtpAppender.setLayout(buildPatternLayout(DEFAULT_PATTERN));
        smtpAppender.start();
        logger.addAppender(smtpAppender);
        String msg0 = "hello zero";
        logger.debug(msg0);
        logger.error("error zero");

        String msg1 = "hello one";
        logger.debug(msg1);
        logger.error("error one");

        Thread.yield();
        int oldCount = 0;
        int expectedEmailCount = oldCount + 2;
        waitForServerToReceiveEmails(expectedEmailCount);

        MimeMessage[] mma = greenMailServer.getReceivedMessages();
        assertNotNull(mma);
        assertEquals(expectedEmailCount, mma.length);

        MimeMessage mm0 = mma[oldCount];
        MimeMultipart content0 = (MimeMultipart) mm0.getContent();
        @SuppressWarnings("unused")
        String body0 = GreenMailUtil.getBody(content0.getBodyPart(0));

        MimeMessage mm1 = mma[oldCount + 1];
        MimeMultipart content1 = (MimeMultipart) mm1.getContent();
        String body1 = GreenMailUtil.getBody(content1.getBodyPart(0));
        // second body should not contain content from first message
        assertFalse(body1.contains(msg0));
    }

    @Test
    public void multiLineSubjectTruncatedAtFirstNewLine() throws Exception {
        startSMTPServer(NO_SSL);
        String line1 = "line 1 of subject";
        String subject = line1 + "\nline 2 of subject\n";
        buildSMTPAppender(subject, ASYNCHRONOUS);

        smtpAppender.setLayout(buildPatternLayout(DEFAULT_PATTERN));
        smtpAppender.start();
        logger.addAppender(smtpAppender);
        logger.debug("hello");
        logger.error("en error", new Exception("an exception"));

        Thread.yield();
        waitUntilEmailIsSent();
        waitForServerToReceiveEmails(1);

        MimeMessage[] mma = greenMailServer.getReceivedMessages();
        assertEquals(1, mma.length);
        assertEquals(line1, mma[0].getSubject());
    }

    @Test
    public void authenticated() throws Exception {
        startSMTPServer(NO_SSL);
        buildSMTPAppender("testMultipleTo", SYNCHRONOUS);
        smtpAppender.setUsername(REQUIRED_USERNAME);
        smtpAppender.setPassword(REQUIRED_PASSWORD);

        smtpAppender.setLayout(buildPatternLayout(DEFAULT_PATTERN));
        smtpAppender.start();

        logger.addAppender(smtpAppender);
        logger.debug("authenticated");
        logger.error("authenticated en error", new Exception("an exception"));

        waitUntilEmailIsSent();
        waitForServerToReceiveEmails(1);

        MimeMessage[] mma = greenMailServer.getReceivedMessages();
        assertNotNull(mma);
        assertTrue(mma.length == 1, "body should not be empty");
    }

    void setSystemPropertiesForStartTLS() {
        String PREFIX = "mail.smtp.";
        System.setProperty(PREFIX + "starttls.enable", "true");
        System.setProperty(PREFIX + "socketFactory.class", DummySSLSocketFactory.class.getName());
        System.setProperty(PREFIX + "socketFactory.fallback", "false");
    }

    void unsetSystemPropertiesForStartTLS() {
        String PREFIX = "mail.smtp.";
        System.clearProperty(PREFIX + "starttls.enable");
        System.clearProperty(PREFIX + "socketFactory.class");
        System.clearProperty(PREFIX + "socketFactory.fallback");
    }

    @Test
    public void authenticatedSSL() throws Exception {

        try {
            // without setting this property, the SSL handshake fails with newer icegreen, angus versions
            System.setProperty(CHECK_SERVER_IDENTITY_KEY, "false");
            setSystemPropertiesForStartTLS();
            //System.setProperty("greenmail.smtps.host",  "127.0.0.1");
            startSMTPServer(WITH_SSL);
            buildSMTPAppender("testMultipleTo", SYNCHRONOUS);
            smtpAppender.setUsername(REQUIRED_USERNAME);
            smtpAppender.setPassword(REQUIRED_PASSWORD);
            smtpAppender.setSTARTTLS(true);
            smtpAppender.setLayout(buildPatternLayout(DEFAULT_PATTERN));
            smtpAppender.start();

            logger.addAppender(smtpAppender);
            logger.debug("authenticated");
            logger.error("authenticated en error", new Exception("an exception"));

            waitUntilEmailIsSent();
            waitForServerToReceiveEmails(1);

            MimeMessage[] mma = greenMailServer.getReceivedMessages();
            assertNotNull(mma);
            assertTrue(mma.length == 1, "body should not be empty");
        } finally {
            System.clearProperty(CHECK_SERVER_IDENTITY_KEY);
            unsetSystemPropertiesForStartTLS();
        }
    }

    // ==============================================================================
    // IGNORED
    // ==============================================================================
    static String GMAIL_USER_NAME = "xx@gmail.com";
    static String GMAIL_PASSWORD = "xxx";

    @Disabled
    @Test
    public void authenticatedGmailStartTLS() throws Exception {
        smtpAppender.setSMTPHost("smtp.gmail.com");
        smtpAppender.setSMTPPort(587);
        smtpAppender.setAsynchronousSending(false);
        smtpAppender.addTo(GMAIL_USER_NAME);

        smtpAppender.setSTARTTLS(true);
        smtpAppender.setUsername(GMAIL_USER_NAME);
        smtpAppender.setPassword(GMAIL_PASSWORD);

        smtpAppender.setLayout(buildPatternLayout(DEFAULT_PATTERN));
        smtpAppender.setSubject("authenticatedGmailStartTLS - %level %logger{20} - %m");
        smtpAppender.start();
        Logger logger = loggerContext.getLogger("authenticatedGmailSTARTTLS");
        logger.addAppender(smtpAppender);
        logger.debug("authenticatedGmailStartTLS =- hello");
        logger.error("en error", new Exception("an exception"));

        StatusPrinter.print(loggerContext);
    }

    @Disabled
    @Test
    public void authenticatedGmail_SSL() throws Exception {
        smtpAppender.setSMTPHost("smtp.gmail.com");
        smtpAppender.setSMTPPort(465);
        smtpAppender.setSubject("authenticatedGmail_SSL - %level %logger{20} - %m");
        smtpAppender.addTo(GMAIL_USER_NAME);
        smtpAppender.setSSL(true);
        smtpAppender.setUsername(GMAIL_USER_NAME);
        smtpAppender.setPassword(GMAIL_PASSWORD);
        smtpAppender.setAsynchronousSending(false);
        smtpAppender.setLayout(buildPatternLayout(DEFAULT_PATTERN));
        smtpAppender.start();
        Logger logger = loggerContext.getLogger("authenticatedGmail_SSL");
        logger.addAppender(smtpAppender);
        logger.debug("hello" + new java.util.Date());
        logger.error("en error", new Exception("an exception"));

        StatusPrinter.print(loggerContext);

    }
}