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