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