1   /*
2    * Logback: the reliable, generic, fast and flexible logging framework.
3    * Copyright (C) 1999-2026, QOS.ch. All rights reserved.
4    *
5    * This program and the accompanying materials are dual-licensed under
6    * either the terms of the Eclipse Public License v2.0 as published by
7    * the Eclipse Foundation
8    *
9    *   or (per the licensee's choosing)
10   *
11   * under the terms of the GNU Lesser General Public License version 2.1
12   * as published by the Free Software Foundation.
13   */
14  package ch.qos.logback.classic.blackbox.net;
15  
16  import java.io.ByteArrayInputStream;
17  import java.io.ByteArrayOutputStream;
18  import java.io.IOException;
19  import java.io.InputStream;
20  import java.util.concurrent.ExecutorService;
21  import java.util.concurrent.TimeUnit;
22  
23  import ch.qos.logback.classic.Level;
24  import ch.qos.logback.classic.blackbox.BlackboxClassicTestConstants;
25  import ch.qos.logback.classic.net.SMTPAppender;
26  import ch.qos.logback.classic.util.LogbackMDCAdapter;
27  import ch.qos.logback.core.util.EnvUtil;
28  import com.icegreen.greenmail.util.*;
29  import org.dom4j.DocumentException;
30  import org.dom4j.io.SAXReader;
31  import org.junit.jupiter.api.AfterEach;
32  import org.junit.jupiter.api.BeforeEach;
33  import org.junit.jupiter.api.Disabled;
34  import org.junit.jupiter.api.Test;
35  import org.slf4j.LoggerFactory;
36  import org.slf4j.MDC;
37  
38  //import ch.qos.logback.classic.ClassicTestConstants;
39  import ch.qos.logback.classic.Logger;
40  import ch.qos.logback.classic.LoggerContext;
41  import ch.qos.logback.classic.PatternLayout;
42  import ch.qos.logback.classic.html.HTMLLayout;
43  import ch.qos.logback.classic.blackbox.html.XHTMLEntityResolver;
44  import ch.qos.logback.classic.joran.JoranConfigurator;
45  import ch.qos.logback.classic.spi.ILoggingEvent;
46  import ch.qos.logback.core.Layout;
47  import ch.qos.logback.core.joran.spi.JoranException;
48  import ch.qos.logback.core.status.OnConsoleStatusListener;
49  import ch.qos.logback.core.testUtil.RandomUtil;
50  import ch.qos.logback.core.util.StatusListenerConfigHelper;
51  import ch.qos.logback.core.util.StatusPrinter;
52  import jakarta.mail.MessagingException;
53  import jakarta.mail.internet.MimeMessage;
54  import jakarta.mail.internet.MimeMultipart;
55  
56  import static org.junit.jupiter.api.Assertions.*;
57  
58  public class SMTPAppender_GreenTest {
59  
60      static boolean NO_SSL = false;
61      static boolean WITH_SSL = true;
62  
63      static final String HEADER = "HEADER\n";
64      static final String FOOTER = "FOOTER\n";
65      static final String DEFAULT_PATTERN = "%-4relative %mdc [%thread] %-5level %class - %msg%n";
66  
67      static final String JAVAX_NET_DEBUG_KEY = "javax.net.debug";
68      static final String SSH_HANDSHAKE_DEBUG_VAL =  "ssl:handshake";
69      // for use in authenticated SSL tests only (to avoid SSL handshake failures)
70      static final String CHECK_SERVER_IDENTITY_KEY = "mail.smtp.ssl.checkserveridentity";
71  
72      static final boolean SYNCHRONOUS = false;
73      static final boolean ASYNCHRONOUS = true;
74      static int TIMEOUT = 3000;
75  
76      int port = RandomUtil.getRandomServerPort();
77      // GreenMail cannot be static. As a shared server induces race conditions
78      GreenMail greenMailServer;
79  
80      SMTPAppender smtpAppender;
81      LoggerContext loggerContext = new LoggerContext();
82      LogbackMDCAdapter logbackMDCAdapter = new LogbackMDCAdapter();
83      Logger logger = loggerContext.getLogger(this.getClass());
84  
85      static String REQUIRED_USERNAME = "alice";
86      static String REQUIRED_PASSWORD = "alicepass";
87  
88  
89      @BeforeEach
90      public void setUp() throws Exception {
91          loggerContext.setMDCAdapter(logbackMDCAdapter);
92          StatusListenerConfigHelper.addOnConsoleListenerInstance(loggerContext, new OnConsoleStatusListener());
93          setGlobalLogbackLoggerForGreenmail(Level.INFO);
94      }
95  
96  
97      @AfterEach
98      public void tearDown() throws Exception {
99          greenMailServer.stop();
100         setGlobalLogbackLoggerForGreenmail(null);
101     }
102 
103     private static void setGlobalLogbackLoggerForGreenmail(Level level) {
104         Logger logbackLogger = (Logger) LoggerFactory.getLogger("com.icegreen.greenmail");
105         logbackLogger.setLevel(level);
106     }
107 
108 
109     void startSMTPServer(boolean withSSL) {
110         ServerSetup serverSetup;
111 
112         if (withSSL) {
113             serverSetup = new ServerSetup(port, null, ServerSetup.PROTOCOL_SMTPS);
114         } else {
115             serverSetup = new ServerSetup(port, null, ServerSetup.PROTOCOL_SMTP);
116         }
117         greenMailServer = new GreenMail(serverSetup);
118         // user password is checked for the specified user ONLY
119         greenMailServer.setUser(REQUIRED_USERNAME, REQUIRED_PASSWORD);
120         greenMailServer.start();
121         // give the server a head start
122         try {
123             Thread.sleep(10);
124         } catch (InterruptedException e) {
125         }
126     }
127 
128 
129     void buildSMTPAppender(String subject, boolean synchronicity) throws Exception {
130         smtpAppender = new SMTPAppender();
131         smtpAppender.setContext(loggerContext);
132         smtpAppender.setName("smtp");
133         smtpAppender.setFrom("user@host.dom");
134         smtpAppender.setSMTPHost("localhost");
135         smtpAppender.setSMTPPort(port);
136         smtpAppender.setSubject(subject);
137         smtpAppender.addTo("nospam@qos.ch");
138         smtpAppender.setAsynchronousSending(synchronicity);
139     }
140 
141     private Layout<ILoggingEvent> buildPatternLayout(String pattern) {
142         PatternLayout layout = new PatternLayout();
143         layout.setContext(loggerContext);
144         layout.setFileHeader(HEADER);
145         layout.setOutputPatternAsHeader(false);
146         layout.setPattern(pattern);
147         layout.setFileFooter(FOOTER);
148         layout.start();
149         return layout;
150     }
151 
152     private Layout<ILoggingEvent> buildHTMLLayout() {
153         HTMLLayout layout = new HTMLLayout();
154         layout.setContext(loggerContext);
155         layout.setPattern("%level%class%msg");
156         layout.start();
157         return layout;
158     }
159 
160     private void waitForServerToReceiveEmails(int emailCount) throws InterruptedException {
161         greenMailServer.waitForIncomingEmail(5000, emailCount);
162     }
163 
164     private MimeMultipart verifyAndExtractMimeMultipart(String subject)
165             throws MessagingException, IOException, InterruptedException {
166         int oldCount = 0;
167         int expectedEmailCount = 1;
168         // wait for the server to receive the messages
169         waitForServerToReceiveEmails(expectedEmailCount);
170         MimeMessage[] mma = greenMailServer.getReceivedMessages();
171         assertNotNull(mma);
172         assertEquals(expectedEmailCount, mma.length);
173         MimeMessage mm = mma[oldCount];
174         // http://jira.qos.ch/browse/LBCLASSIC-67
175         assertEquals(subject, mm.getSubject());
176         return (MimeMultipart) mm.getContent();
177     }
178 
179 
180 
181     void waitUntilEmailIsSent() throws InterruptedException {
182         ExecutorService es = loggerContext.getExecutorService();
183         es.shutdown();
184         boolean terminated = es.awaitTermination(TIMEOUT, TimeUnit.MILLISECONDS);
185 
186         // this assertion may be needlessly strict, skipped on MacOS
187         if(!terminated && !EnvUtil.isMacOs()) {
188             fail("executor elapsed before accorded delay " + System.getProperty("os.name"));
189         }
190 
191     }
192 
193     @Test
194     public void synchronousSmoke() throws Exception {
195         startSMTPServer(NO_SSL);
196         String subject = "synchronousSmoke";
197         buildSMTPAppender(subject, SYNCHRONOUS);
198 
199         smtpAppender.setLayout(buildPatternLayout(DEFAULT_PATTERN));
200 
201         smtpAppender.start();
202 
203         logger.addAppender(smtpAppender);
204         logger.debug("hello");
205         logger.error("en error", new Exception("an exception"));
206 
207         MimeMultipart mp = verifyAndExtractMimeMultipart(subject);
208         String body = GreenMailUtil.getBody(mp.getBodyPart(0));
209         assertTrue(body.startsWith(HEADER.trim()));
210         assertTrue(body.endsWith(FOOTER.trim()));
211     }
212 
213     @Test
214     public void asynchronousSmoke() throws Exception {
215         startSMTPServer(NO_SSL);
216 
217         String subject = "asynchronousSmoke";
218         buildSMTPAppender(subject, ASYNCHRONOUS);
219         smtpAppender.setLayout(buildPatternLayout(DEFAULT_PATTERN));
220         smtpAppender.start();
221 
222         logger.addAppender(smtpAppender);
223         logger.debug("hello");
224         logger.error("en error", new Exception("an exception"));
225 
226         waitUntilEmailIsSent();
227         MimeMultipart mp = verifyAndExtractMimeMultipart(subject);
228         String body = GreenMailUtil.getBody(mp.getBodyPart(0));
229         assertTrue(body.startsWith(HEADER.trim()));
230         assertTrue(body.endsWith(FOOTER.trim()));
231     }
232 
233     // See also http://jira.qos.ch/browse/LOGBACK-734
234     @Test
235     public void callerDataShouldBeCorrectlySetWithAsynchronousSending() throws Exception {
236         startSMTPServer(NO_SSL);
237         String subject = "LOGBACK-734";
238         buildSMTPAppender("LOGBACK-734", ASYNCHRONOUS);
239         smtpAppender.setLayout(buildPatternLayout(DEFAULT_PATTERN));
240         smtpAppender.setIncludeCallerData(true);
241         smtpAppender.start();
242         logger.addAppender(smtpAppender);
243         logger.debug("LOGBACK-734");
244         logger.error("callerData", new Exception("ShouldBeCorrectlySetWithAsynchronousSending"));
245 
246         waitUntilEmailIsSent();
247         MimeMultipart mp = verifyAndExtractMimeMultipart(subject);
248         String body = GreenMailUtil.getBody(mp.getBodyPart(0));
249         assertTrue(body.contains("DEBUG " + this.getClass().getName() + " - LOGBACK-734"), "actual [" + body + "]");
250     }
251 
252     // lost MDC
253     @Test
254     public void LOGBACK_352() throws Exception {
255         startSMTPServer(NO_SSL);
256         String subject = "LOGBACK_352";
257         buildSMTPAppender(subject, SYNCHRONOUS);
258         smtpAppender.setAsynchronousSending(false);
259         smtpAppender.setLayout(buildPatternLayout(DEFAULT_PATTERN));
260         smtpAppender.start();
261         logger.addAppender(smtpAppender);
262         logbackMDCAdapter.put("key", "val");
263         logger.debug("LBCLASSIC_104");
264         logbackMDCAdapter.clear();
265         logger.error("en error", new Exception("test"));
266 
267         MimeMultipart mp = verifyAndExtractMimeMultipart(subject);
268         String body = GreenMailUtil.getBody(mp.getBodyPart(0));
269         assertTrue(body.startsWith(HEADER.trim()));
270         System.out.println(body);
271         assertTrue(body.contains("key=val"));
272         assertTrue(body.endsWith(FOOTER.trim()));
273     }
274 
275     @Test
276     public void html() throws Exception {
277         startSMTPServer(NO_SSL);
278         String subject = "html";
279         buildSMTPAppender(subject, SYNCHRONOUS);
280         smtpAppender.setAsynchronousSending(false);
281         smtpAppender.setLayout(buildHTMLLayout());
282         smtpAppender.start();
283         logger.addAppender(smtpAppender);
284         logger.debug("html");
285         logger.error("en error", new Exception("an exception"));
286 
287         MimeMultipart mp = verifyAndExtractMimeMultipart(subject);
288 
289         // verifyAndExtractMimeMultipart strict adherence to xhtml1-strict.dtd
290         SAXReader reader = new SAXReader();
291         reader.setValidation(true);
292         reader.setEntityResolver(new XHTMLEntityResolver());
293         byte[] messageBytes = getAsByteArray(mp.getBodyPart(0).getInputStream());
294         ByteArrayInputStream bais = new ByteArrayInputStream(messageBytes);
295         try {
296             reader.read(bais);
297         } catch (DocumentException de) {
298             System.out.println("incoming message:");
299             System.out.println(new String(messageBytes));
300             throw de;
301         }
302         System.out.println("incoming message:");
303         System.out.println(new String(messageBytes));
304     }
305 
306     private byte[] getAsByteArray(InputStream inputStream) throws IOException {
307         ByteArrayOutputStream baos = new ByteArrayOutputStream();
308 
309         byte[] buffer = new byte[1024];
310         int n = -1;
311         while ((n = inputStream.read(buffer)) != -1) {
312             baos.write(buffer, 0, n);
313         }
314         return baos.toByteArray();
315     }
316 
317     private void configure(String file) throws JoranException {
318         JoranConfigurator jc = new JoranConfigurator();
319         jc.setContext(loggerContext);
320         loggerContext.putProperty("port", "" + port);
321         jc.doConfigure(file);
322     }
323 
324     @Test
325     public void testCustomEvaluator() throws Exception {
326         startSMTPServer(NO_SSL);
327         configure(BlackboxClassicTestConstants.JORAN_INPUT_PREFIX + "smtp/customEvaluator.xml");
328 
329         logger.debug("test");
330         String msg2 = "CustomEvaluator";
331         logger.debug(msg2);
332         logger.debug("invisible");
333         waitUntilEmailIsSent();
334         MimeMultipart mp = verifyAndExtractMimeMultipart(
335                 "testCustomEvaluator " + this.getClass().getName() + " - " + msg2);
336         String body = GreenMailUtil.getBody(mp.getBodyPart(0));
337         assertEquals("testCustomEvaluator", body);
338     }
339 
340     @Test
341     public void testCustomBufferSize() throws Exception {
342         startSMTPServer(NO_SSL);
343         configure(BlackboxClassicTestConstants.JORAN_INPUT_PREFIX + "smtp/customBufferSize.xml");
344 
345         logger.debug("invisible1");
346         logger.debug("invisible2");
347         String msg = "hello";
348         logger.error(msg);
349         waitUntilEmailIsSent();
350         MimeMultipart mp = verifyAndExtractMimeMultipart(
351                 "testCustomBufferSize " + this.getClass().getName() + " - " + msg);
352         String body = GreenMailUtil.getBody(mp.getBodyPart(0));
353         assertEquals(msg, body);
354     }
355 
356     // this test fails intermittently on Jenkins.
357     @Test
358     public void testMultipleTo() throws Exception {
359         startSMTPServer(NO_SSL);
360         buildSMTPAppender("testMultipleTo", SYNCHRONOUS);
361         smtpAppender.setLayout(buildPatternLayout(DEFAULT_PATTERN));
362         // buildSMTPAppender() already added one destination address
363         smtpAppender.addTo("Test <test@example.com>, other-test@example.com");
364         smtpAppender.start();
365         logger.addAppender(smtpAppender);
366         logger.debug("testMultipleTo hello");
367         logger.error("testMultipleTo en error", new Exception("an exception"));
368         Thread.yield();
369         int expectedEmailCount = 3;
370         waitForServerToReceiveEmails(expectedEmailCount);
371         MimeMessage[] mma = greenMailServer.getReceivedMessages();
372         assertNotNull(mma);
373         assertEquals(expectedEmailCount, mma.length);
374     }
375 
376     // http://jira.qos.ch/browse/LBCLASSIC-221
377     @Test
378     public void bufferShouldBeResetBetweenMessages() throws Exception {
379         startSMTPServer(NO_SSL);
380         buildSMTPAppender("bufferShouldBeResetBetweenMessages", SYNCHRONOUS);
381         smtpAppender.setLayout(buildPatternLayout(DEFAULT_PATTERN));
382         smtpAppender.start();
383         logger.addAppender(smtpAppender);
384         String msg0 = "hello zero";
385         logger.debug(msg0);
386         logger.error("error zero");
387 
388         String msg1 = "hello one";
389         logger.debug(msg1);
390         logger.error("error one");
391 
392         Thread.yield();
393         int oldCount = 0;
394         int expectedEmailCount = oldCount + 2;
395         waitForServerToReceiveEmails(expectedEmailCount);
396 
397         MimeMessage[] mma = greenMailServer.getReceivedMessages();
398         assertNotNull(mma);
399         assertEquals(expectedEmailCount, mma.length);
400 
401         MimeMessage mm0 = mma[oldCount];
402         MimeMultipart content0 = (MimeMultipart) mm0.getContent();
403         @SuppressWarnings("unused")
404         String body0 = GreenMailUtil.getBody(content0.getBodyPart(0));
405 
406         MimeMessage mm1 = mma[oldCount + 1];
407         MimeMultipart content1 = (MimeMultipart) mm1.getContent();
408         String body1 = GreenMailUtil.getBody(content1.getBodyPart(0));
409         // second body should not contain content from first message
410         assertFalse(body1.contains(msg0));
411     }
412 
413     @Test
414     public void multiLineSubjectTruncatedAtFirstNewLine() throws Exception {
415         startSMTPServer(NO_SSL);
416         String line1 = "line 1 of subject";
417         String subject = line1 + "\nline 2 of subject\n";
418         buildSMTPAppender(subject, ASYNCHRONOUS);
419 
420         smtpAppender.setLayout(buildPatternLayout(DEFAULT_PATTERN));
421         smtpAppender.start();
422         logger.addAppender(smtpAppender);
423         logger.debug("hello");
424         logger.error("en error", new Exception("an exception"));
425 
426         Thread.yield();
427         waitUntilEmailIsSent();
428         waitForServerToReceiveEmails(1);
429 
430         MimeMessage[] mma = greenMailServer.getReceivedMessages();
431         assertEquals(1, mma.length);
432         assertEquals(line1, mma[0].getSubject());
433     }
434 
435     @Test
436     public void authenticated() throws Exception {
437         startSMTPServer(NO_SSL);
438         buildSMTPAppender("testMultipleTo", SYNCHRONOUS);
439         smtpAppender.setUsername(REQUIRED_USERNAME);
440         smtpAppender.setPassword(REQUIRED_PASSWORD);
441 
442         smtpAppender.setLayout(buildPatternLayout(DEFAULT_PATTERN));
443         smtpAppender.start();
444 
445         logger.addAppender(smtpAppender);
446         logger.debug("authenticated");
447         logger.error("authenticated en error", new Exception("an exception"));
448 
449         waitUntilEmailIsSent();
450         waitForServerToReceiveEmails(1);
451 
452         MimeMessage[] mma = greenMailServer.getReceivedMessages();
453         assertNotNull(mma);
454         assertTrue(mma.length == 1, "body should not be empty");
455     }
456 
457     void setSystemPropertiesForStartTLS() {
458         String PREFIX = "mail.smtp.";
459         System.setProperty(PREFIX + "starttls.enable", "true");
460         System.setProperty(PREFIX + "socketFactory.class", DummySSLSocketFactory.class.getName());
461         System.setProperty(PREFIX + "socketFactory.fallback", "false");
462     }
463 
464     void unsetSystemPropertiesForStartTLS() {
465         String PREFIX = "mail.smtp.";
466         System.clearProperty(PREFIX + "starttls.enable");
467         System.clearProperty(PREFIX + "socketFactory.class");
468         System.clearProperty(PREFIX + "socketFactory.fallback");
469     }
470 
471     @Test
472     public void authenticatedSSL() throws Exception {
473 
474         try {
475             // without setting this property, the SSL handshake fails with newer icegreen, angus versions
476             System.setProperty(CHECK_SERVER_IDENTITY_KEY, "false");
477             setSystemPropertiesForStartTLS();
478             //System.setProperty("greenmail.smtps.host",  "127.0.0.1");
479             startSMTPServer(WITH_SSL);
480             buildSMTPAppender("testMultipleTo", SYNCHRONOUS);
481             smtpAppender.setUsername(REQUIRED_USERNAME);
482             smtpAppender.setPassword(REQUIRED_PASSWORD);
483             smtpAppender.setSTARTTLS(true);
484             smtpAppender.setLayout(buildPatternLayout(DEFAULT_PATTERN));
485             smtpAppender.start();
486 
487             logger.addAppender(smtpAppender);
488             logger.debug("authenticated");
489             logger.error("authenticated en error", new Exception("an exception"));
490 
491             waitUntilEmailIsSent();
492             waitForServerToReceiveEmails(1);
493 
494             MimeMessage[] mma = greenMailServer.getReceivedMessages();
495             assertNotNull(mma);
496             assertTrue(mma.length == 1, "body should not be empty");
497         } finally {
498             System.clearProperty(CHECK_SERVER_IDENTITY_KEY);
499             unsetSystemPropertiesForStartTLS();
500         }
501     }
502 
503     // ==============================================================================
504     // IGNORED
505     // ==============================================================================
506     static String GMAIL_USER_NAME = "xx@gmail.com";
507     static String GMAIL_PASSWORD = "xxx";
508 
509     @Disabled
510     @Test
511     public void authenticatedGmailStartTLS() throws Exception {
512         smtpAppender.setSMTPHost("smtp.gmail.com");
513         smtpAppender.setSMTPPort(587);
514         smtpAppender.setAsynchronousSending(false);
515         smtpAppender.addTo(GMAIL_USER_NAME);
516 
517         smtpAppender.setSTARTTLS(true);
518         smtpAppender.setUsername(GMAIL_USER_NAME);
519         smtpAppender.setPassword(GMAIL_PASSWORD);
520 
521         smtpAppender.setLayout(buildPatternLayout(DEFAULT_PATTERN));
522         smtpAppender.setSubject("authenticatedGmailStartTLS - %level %logger{20} - %m");
523         smtpAppender.start();
524         Logger logger = loggerContext.getLogger("authenticatedGmailSTARTTLS");
525         logger.addAppender(smtpAppender);
526         logger.debug("authenticatedGmailStartTLS =- hello");
527         logger.error("en error", new Exception("an exception"));
528 
529         StatusPrinter.print(loggerContext);
530     }
531 
532     @Disabled
533     @Test
534     public void authenticatedGmail_SSL() throws Exception {
535         smtpAppender.setSMTPHost("smtp.gmail.com");
536         smtpAppender.setSMTPPort(465);
537         smtpAppender.setSubject("authenticatedGmail_SSL - %level %logger{20} - %m");
538         smtpAppender.addTo(GMAIL_USER_NAME);
539         smtpAppender.setSSL(true);
540         smtpAppender.setUsername(GMAIL_USER_NAME);
541         smtpAppender.setPassword(GMAIL_PASSWORD);
542         smtpAppender.setAsynchronousSending(false);
543         smtpAppender.setLayout(buildPatternLayout(DEFAULT_PATTERN));
544         smtpAppender.start();
545         Logger logger = loggerContext.getLogger("authenticatedGmail_SSL");
546         logger.addAppender(smtpAppender);
547         logger.debug("hello" + new java.util.Date());
548         logger.error("en error", new Exception("an exception"));
549 
550         StatusPrinter.print(loggerContext);
551 
552     }
553 }