View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.mail2.jakarta;
18  
19  import static org.junit.jupiter.api.Assertions.assertEquals;
20  import static org.junit.jupiter.api.Assertions.assertFalse;
21  import static org.junit.jupiter.api.Assertions.assertTrue;
22  import static org.junit.jupiter.api.Assertions.fail;
23  
24  import java.io.BufferedOutputStream;
25  import java.io.ByteArrayOutputStream;
26  import java.io.File;
27  import java.io.IOException;
28  import java.net.URL;
29  import java.util.Date;
30  import java.util.Enumeration;
31  import java.util.List;
32  
33  import org.apache.commons.mail2.jakarta.settings.EmailConfiguration;
34  import org.apache.commons.mail2.jakarta.util.MimeMessageUtils;
35  import org.junit.jupiter.api.AfterEach;
36  import org.junit.jupiter.api.BeforeEach;
37  import org.mockito.Mockito;
38  import org.subethamail.wiser.Wiser;
39  import org.subethamail.wiser.WiserMessage;
40  
41  import jakarta.activation.DataHandler;
42  import jakarta.mail.Header;
43  import jakarta.mail.MessagingException;
44  import jakarta.mail.Multipart;
45  import jakarta.mail.internet.InternetAddress;
46  import jakarta.mail.internet.MimeMessage;
47  
48  /**
49   * Base test case for Email test classes.
50   */
51  public abstract class AbstractEmailTest {
52      /** Padding at end of body added by wiser/send */
53      public static final int BODY_END_PAD = 3;
54  
55      /** Padding at start of body added by wiser/send */
56      public static final int BODY_START_PAD = 2;
57  
58      /** Line separator in email messages */
59      private static final String LINE_SEPARATOR = "\r\n";
60  
61      /** Default port */
62      private static int mailServerPort = 2500;
63  
64      /** Counter for creating a file name */
65      private static int fileNameCounter;
66  
67      /** The fake Wiser email server */
68      protected Wiser fakeMailServer;
69  
70      /** Mail server used for testing */
71      protected String strTestMailServer = "localhost";
72  
73      /** From address for the test email */
74      protected String strTestMailFrom = "test_from@apache.org";
75  
76      /** Destination address for the test email */
77      protected String strTestMailTo = "test_to@apache.org";
78  
79      /** Mailserver username (set if needed) */
80      protected String strTestUser = "user";
81  
82      /** Mailserver strTestPasswd (set if needed) */
83      protected String strTestPasswd = "password";
84  
85      /** URL to used to test URL attachments (Must be valid) */
86      protected String strTestURL = EmailConfiguration.TEST_URL;
87  
88      /** Test characters acceptable to email */
89      protected String[] testCharsValid = { " ", "a", "A", "\uc5ec", "0123456789", "012345678901234567890" };
90  
91      /** Test characters not acceptable to email */
92      protected String[] endOfLineCombinations = { "\n", "\r", "\r\n", "\n\r", };
93  
94      /** Array of test strings */
95      protected String[] testCharsNotValid = { "", null };
96  
97      /** Where to save email output **/
98      private File emailOutputDir;
99  
100     /**
101      * Create a mocked URL object which always throws an IOException when the openStream() method is called.
102      * <p>
103      * Several ISPs do resolve invalid URLs like {@code https://example.invalid} to some error page causing tests to fail otherwise.
104      * </p>
105      *
106      * @return an invalid URL
107      */
108     @SuppressWarnings("resource") // openStream() returns null.
109     protected URL createInvalidURL() throws Exception {
110         URL url = new URL("http://example.invalid");
111         url = Mockito.spy(url);
112         Mockito.doThrow(new IOException("Mocked IOException")).when(url).openStream();
113         return url;
114     }
115 
116     /**
117      * Initializes the stub mail server. Fails if the server cannot be proven to have started. If the server is already started, this method returns without
118      * changing the state of the server.
119      */
120     public void getMailServer() {
121         if (fakeMailServer == null || isMailServerStopped(fakeMailServer)) {
122             mailServerPort++;
123 
124             fakeMailServer = Wiser.port(getMailServerPort());
125             fakeMailServer.start();
126 
127             assertFalse(isMailServerStopped(fakeMailServer), "fake mail server didn't start");
128 
129             final Date dtStartWait = new Date();
130             while (isMailServerStopped(fakeMailServer)) {
131                 // test for connected
132                 if (fakeMailServer != null && !isMailServerStopped(fakeMailServer)) {
133                     break;
134                 }
135 
136                 // test for timeout
137                 if (dtStartWait.getTime() + EmailConfiguration.TIME_OUT <= new Date().getTime()) {
138                     fail("Mail server failed to start");
139                 }
140             }
141         }
142     }
143 
144     /**
145      * Gets the mail server port.
146      *
147      * @return the port the server is running on.
148      */
149     protected int getMailServerPort() {
150         return mailServerPort;
151     }
152 
153     /**
154      * @param intMsgNo the message to retrieve
155      * @return message as string
156      */
157     public String getMessageAsString(final int intMsgNo) {
158         final List<?> receivedMessages = fakeMailServer.getMessages();
159         assertTrue(receivedMessages.size() >= intMsgNo, "mail server didn't get enough messages");
160 
161         final WiserMessage emailMessage = (WiserMessage) receivedMessages.get(intMsgNo);
162 
163         if (emailMessage != null) {
164             try {
165                 return serializeEmailMessage(emailMessage);
166             } catch (final Exception e) {
167                 // ignore, since the test will fail on an empty string return
168             }
169         }
170         fail("Message not found");
171         return "";
172     }
173 
174     /**
175      * Returns a string representation of the message body. If the message body cannot be read, an empty string is returned.
176      *
177      * @param wiserMessage The wiser message from which to extract the message body
178      * @return The string representation of the message body
179      * @throws IOException Thrown while serializing the body from {@link DataHandler#writeTo(java.io.OutputStream)}.
180      */
181     private String getMessageBody(final WiserMessage wiserMessage) throws IOException {
182         if (wiserMessage == null) {
183             return "";
184         }
185 
186         byte[] messageBody = null;
187 
188         try {
189             final MimeMessage message = wiserMessage.getMimeMessage();
190             messageBody = getMessageBodyBytes(message);
191         } catch (final MessagingException e) {
192             // Thrown while getting the body content from
193             // {@link MimeMessage#getDataHandler()}
194             final IllegalStateException rethrow = new IllegalStateException("couldn't process MimeMessage from WiserMessage in getMessageBody()");
195             rethrow.initCause(e);
196             throw rethrow;
197         }
198 
199         return messageBody != null ? new String(messageBody).intern() : "";
200     }
201 
202     /**
203      * Gets the byte making up the body of the message.
204      *
205      * @param mimeMessage The mime message from which to extract the body.
206      * @return A byte array representing the message body
207      * @throws IOException        Thrown while serializing the body from {@link DataHandler#writeTo(java.io.OutputStream)}.
208      * @throws MessagingException Thrown while getting the body content from {@link MimeMessage#getDataHandler()}
209      */
210     private byte[] getMessageBodyBytes(final MimeMessage mimeMessage) throws IOException, MessagingException {
211         final DataHandler dataHandler = mimeMessage.getDataHandler();
212         final ByteArrayOutputStream byteArrayOutStream = new ByteArrayOutputStream();
213         final BufferedOutputStream buffOs = new BufferedOutputStream(byteArrayOutStream);
214         dataHandler.writeTo(buffOs);
215         buffOs.flush();
216 
217         return byteArrayOutStream.toByteArray();
218     }
219 
220     /**
221      * Checks if an email server is running at the address stored in the {@code fakeMailServer}.
222      *
223      * @param fakeMailServer The server from which the address is picked up.
224      * @return {@code true} if the server claims to be running
225      */
226     protected boolean isMailServerStopped(final Wiser fakeMailServer) {
227         return !fakeMailServer.getServer().isRunning();
228     }
229 
230     /**
231      * Safe a mail to a file using a more or less unique file name.
232      *
233      * @param email email
234      * @throws IOException        writing the email failed
235      * @throws MessagingException writing the email failed
236      */
237     protected void saveEmailToFile(final WiserMessage email) throws IOException, MessagingException {
238         final int currCounter = fileNameCounter++ % 10;
239         final String emailFileName = "email" + new Date().getTime() + "-" + currCounter + ".eml";
240         final File emailFile = new File(emailOutputDir, emailFileName);
241         MimeMessageUtils.writeMimeMessage(email.getMimeMessage(), emailFile);
242     }
243 
244     /**
245      * Serializes the {@link MimeMessage} from the {@code WiserMessage} passed in. The headers are serialized first followed by the message body.
246      *
247      * @param wiserMessage The {@code WiserMessage} to serialize.
248      * @return The string format of the message.
249      * @throws MessagingException
250      * @throws IOException        Thrown while serializing the body from {@link DataHandler#writeTo(java.io.OutputStream)}.
251      * @throws MessagingException Thrown while getting the body content from {@link MimeMessage#getDataHandler()}
252      */
253     private String serializeEmailMessage(final WiserMessage wiserMessage) throws MessagingException, IOException {
254         if (wiserMessage == null) {
255             return "";
256         }
257 
258         final StringBuilder serializedEmail = new StringBuilder();
259         final MimeMessage message = wiserMessage.getMimeMessage();
260 
261         // Serialize the headers
262         for (final Enumeration<?> headers = message.getAllHeaders(); headers.hasMoreElements();) {
263             final Header header = (Header) headers.nextElement();
264             serializedEmail.append(header.getName());
265             serializedEmail.append(": ");
266             serializedEmail.append(header.getValue());
267             serializedEmail.append(LINE_SEPARATOR);
268         }
269 
270         // Serialize the body
271         final byte[] messageBody = getMessageBodyBytes(message);
272 
273         serializedEmail.append(LINE_SEPARATOR);
274         serializedEmail.append(messageBody);
275         serializedEmail.append(LINE_SEPARATOR);
276 
277         return serializedEmail.toString();
278     }
279 
280     @BeforeEach
281     public void setUpAbstractEmailTest() {
282         emailOutputDir = new File("target/test-emails");
283         if (!emailOutputDir.exists()) {
284             emailOutputDir.mkdirs();
285         }
286     }
287 
288     protected void stopServer() {
289         if (fakeMailServer != null) {
290             fakeMailServer.stop();
291         }
292     }
293 
294     @AfterEach
295     public void tearDownEmailTest() {
296         // stop the fake email server (if started)
297         if (fakeMailServer != null && !isMailServerStopped(fakeMailServer)) {
298             fakeMailServer.stop();
299             assertTrue(isMailServerStopped(fakeMailServer), "Mail server didn't stop");
300         }
301 
302         fakeMailServer = null;
303     }
304 
305     /**
306      * Validate the message was sent properly
307      *
308      * @param mailServer     reference to the fake mail server
309      * @param strSubject     expected subject
310      * @param fromAdd        expected from address
311      * @param toAdd          list of expected to addresses
312      * @param ccAdd          list of expected cc addresses
313      * @param bccAdd         list of expected bcc addresses
314      * @param boolSaveToFile true will output to file, false doesn't
315      * @return WiserMessage email to check
316      * @throws IOException Exception
317      */
318     protected WiserMessage validateSend(final Wiser mailServer, final String strSubject, final InternetAddress fromAdd, final List<InternetAddress> toAdd,
319             final List<InternetAddress> ccAdd, final List<InternetAddress> bccAdd, final boolean boolSaveToFile) throws IOException {
320         assertTrue(mailServer.getMessages().size() == 1, "mail server doesn't contain expected message");
321         final WiserMessage emailMessage = mailServer.getMessages().get(0);
322 
323         if (boolSaveToFile) {
324             try {
325                 saveEmailToFile(emailMessage);
326             } catch (final MessagingException e) {
327                 final IllegalStateException rethrow = new IllegalStateException("caught MessagingException during saving the email");
328                 rethrow.initCause(e);
329                 throw rethrow;
330             }
331         }
332 
333         try {
334             // get the MimeMessage
335             final MimeMessage mimeMessage = emailMessage.getMimeMessage();
336 
337             // test subject
338             assertEquals(strSubject, mimeMessage.getHeader("Subject", null), "got wrong subject from mail");
339 
340             // test from address
341             assertEquals(fromAdd.toString(), mimeMessage.getHeader("From", null), "got wrong From: address from mail");
342 
343             // test to address
344             assertTrue(toAdd.toString().contains(mimeMessage.getHeader("To", null)), "got wrong To: address from mail");
345 
346             // test cc address
347             if (!ccAdd.isEmpty()) {
348                 assertTrue(ccAdd.toString().contains(mimeMessage.getHeader("Cc", null)), "got wrong Cc: address from mail");
349             }
350 
351             // test bcc address
352             if (!bccAdd.isEmpty()) {
353                 assertTrue(bccAdd.toString().contains(mimeMessage.getHeader("Bcc", null)), "got wrong Bcc: address from mail");
354             }
355         } catch (final MessagingException e) {
356             final IllegalStateException rethrow = new IllegalStateException("caught MessagingException in validateSend()");
357             rethrow.initCause(e);
358             throw rethrow;
359         }
360 
361         return emailMessage;
362     }
363 
364     /**
365      * Validate the message was sent properly
366      *
367      * @param mailServer     reference to the fake mail server
368      * @param strSubject     expected subject
369      * @param content        the expected message content
370      * @param fromAdd        expected from address
371      * @param toAdd          list of expected to addresses
372      * @param ccAdd          list of expected cc addresses
373      * @param bccAdd         list of expected bcc addresses
374      * @param boolSaveToFile true will output to file, false doesn't
375      * @throws IOException Exception
376      */
377     protected void validateSend(final Wiser mailServer, final String strSubject, final Multipart content, final InternetAddress fromAdd,
378             final List<InternetAddress> toAdd, final List<InternetAddress> ccAdd, final List<InternetAddress> bccAdd, final boolean boolSaveToFile)
379             throws IOException {
380         // test other properties
381         final WiserMessage emailMessage = validateSend(mailServer, strSubject, fromAdd, toAdd, ccAdd, bccAdd, boolSaveToFile);
382 
383         // test message content
384 
385         // get sent email content
386         final String strSentContent = content.getContentType();
387         // get received email content (chop off the auto-added \n
388         // and -- (front and end)
389         final String emailMessageBody = getMessageBody(emailMessage);
390         final String strMessageBody = emailMessageBody.substring(AbstractEmailTest.BODY_START_PAD, emailMessageBody.length() - AbstractEmailTest.BODY_END_PAD);
391         assertTrue(strMessageBody.contains(strSentContent), "didn't find expected content type in message body");
392     }
393 
394     /**
395      * Validate the message was sent properly
396      *
397      * @param mailServer     reference to the fake mail server
398      * @param strSubject     expected subject
399      * @param strMessage     the expected message as a string
400      * @param fromAdd        expected from address
401      * @param toAdd          list of expected to addresses
402      * @param ccAdd          list of expected cc addresses
403      * @param bccAdd         list of expected bcc addresses
404      * @param boolSaveToFile true will output to file, false doesn't
405      * @throws IOException Exception
406      */
407     protected void validateSend(final Wiser mailServer, final String strSubject, final String strMessage, final InternetAddress fromAdd,
408             final List<InternetAddress> toAdd, final List<InternetAddress> ccAdd, final List<InternetAddress> bccAdd, final boolean boolSaveToFile)
409             throws IOException {
410         // test other properties
411         final WiserMessage emailMessage = validateSend(mailServer, strSubject, fromAdd, toAdd, ccAdd, bccAdd, true);
412 
413         // test message content
414         assertTrue(getMessageBody(emailMessage).contains(strMessage), "didn't find expected message content in message body");
415     }
416 }