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.javax;
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 javax.activation.DataHandler;
34  import javax.mail.Header;
35  import javax.mail.MessagingException;
36  import javax.mail.Multipart;
37  import javax.mail.internet.InternetAddress;
38  import javax.mail.internet.MimeMessage;
39  
40  import org.apache.commons.mail2.javax.settings.EmailConfiguration;
41  import org.apache.commons.mail2.javax.util.MimeMessageUtils;
42  import org.junit.jupiter.api.AfterEach;
43  import org.junit.jupiter.api.BeforeEach;
44  import org.mockito.Mockito;
45  import org.subethamail.wiser.Wiser;
46  import org.subethamail.wiser.WiserMessage;
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 = new Wiser();
125             fakeMailServer.setPort(getMailServerPort());
126             fakeMailServer.start();
127 
128             assertFalse(isMailServerStopped(fakeMailServer), "fake mail server didn't start");
129 
130             final Date dtStartWait = new Date();
131             while (isMailServerStopped(fakeMailServer)) {
132                 // test for connected
133                 if (fakeMailServer != null && !isMailServerStopped(fakeMailServer)) {
134                     break;
135                 }
136 
137                 // test for timeout
138                 if (dtStartWait.getTime() + EmailConfiguration.TIME_OUT <= new Date().getTime()) {
139                     fail("Mail server failed to start");
140                 }
141             }
142         }
143     }
144 
145     /**
146      * Gets the mail server port.
147      *
148      * @return the port the server is running on.
149      */
150     protected int getMailServerPort() {
151         return mailServerPort;
152     }
153 
154     /**
155      * @param intMsgNo the message to retrieve
156      * @return message as string
157      */
158     public String getMessageAsString(final int intMsgNo) {
159         final List<?> receivedMessages = fakeMailServer.getMessages();
160         assertTrue(receivedMessages.size() >= intMsgNo, "mail server didn't get enough messages");
161 
162         final WiserMessage emailMessage = (WiserMessage) receivedMessages.get(intMsgNo);
163 
164         if (emailMessage != null) {
165             try {
166                 return serializeEmailMessage(emailMessage);
167             } catch (final Exception e) {
168                 // ignore, since the test will fail on an empty string return
169             }
170         }
171         fail("Message not found");
172         return "";
173     }
174 
175     /**
176      * Returns a string representation of the message body. If the message body cannot be read, an empty string is returned.
177      *
178      * @param wiserMessage The wiser message from which to extract the message body
179      * @return The string representation of the message body
180      * @throws IOException Thrown while serializing the body from {@link DataHandler#writeTo(java.io.OutputStream)}.
181      */
182     private String getMessageBody(final WiserMessage wiserMessage) throws IOException {
183         if (wiserMessage == null) {
184             return "";
185         }
186 
187         byte[] messageBody = null;
188 
189         try {
190             final MimeMessage message = wiserMessage.getMimeMessage();
191             messageBody = getMessageBodyBytes(message);
192         } catch (final MessagingException e) {
193             // Thrown while getting the body content from
194             // {@link MimeMessage#getDataHandler()}
195             final IllegalStateException rethrow = new IllegalStateException("couldn't process MimeMessage from WiserMessage in getMessageBody()");
196             rethrow.initCause(e);
197             throw rethrow;
198         }
199 
200         return messageBody != null ? new String(messageBody).intern() : "";
201     }
202 
203     /**
204      * Gets the byte making up the body of the message.
205      *
206      * @param mimeMessage The mime message from which to extract the body.
207      * @return A byte array representing the message body
208      * @throws IOException        Thrown while serializing the body from {@link DataHandler#writeTo(java.io.OutputStream)}.
209      * @throws MessagingException Thrown while getting the body content from {@link MimeMessage#getDataHandler()}
210      */
211     private byte[] getMessageBodyBytes(final MimeMessage mimeMessage) throws IOException, MessagingException {
212         final DataHandler dataHandler = mimeMessage.getDataHandler();
213         final ByteArrayOutputStream byteArrayOutStream = new ByteArrayOutputStream();
214         final BufferedOutputStream buffOs = new BufferedOutputStream(byteArrayOutStream);
215         dataHandler.writeTo(buffOs);
216         buffOs.flush();
217 
218         return byteArrayOutStream.toByteArray();
219     }
220 
221     /**
222      * Checks if an email server is running at the address stored in the {@code fakeMailServer}.
223      *
224      * @param fakeMailServer The server from which the address is picked up.
225      * @return {@code true} if the server claims to be running
226      */
227     protected boolean isMailServerStopped(final Wiser fakeMailServer) {
228         return !fakeMailServer.getServer().isRunning();
229     }
230 
231     /**
232      * Safe a mail to a file using a more or less unique file name.
233      *
234      * @param email email
235      * @throws IOException        writing the email failed
236      * @throws MessagingException writing the email failed
237      */
238     protected void saveEmailToFile(final WiserMessage email) throws IOException, MessagingException {
239         final int currCounter = fileNameCounter++ % 10;
240         final String emailFileName = "email" + new Date().getTime() + "-" + currCounter + ".eml";
241         final File emailFile = new File(emailOutputDir, emailFileName);
242         MimeMessageUtils.writeMimeMessage(email.getMimeMessage(), emailFile);
243     }
244 
245     /**
246      * Serializes the {@link MimeMessage} from the {@code WiserMessage} passed in. The headers are serialized first followed by the message body.
247      *
248      * @param wiserMessage The {@code WiserMessage} to serialize.
249      * @return The string format of the message.
250      * @throws MessagingException
251      * @throws IOException        Thrown while serializing the body from {@link DataHandler#writeTo(java.io.OutputStream)}.
252      * @throws MessagingException Thrown while getting the body content from {@link MimeMessage#getDataHandler()}
253      */
254     private String serializeEmailMessage(final WiserMessage wiserMessage) throws MessagingException, IOException {
255         if (wiserMessage == null) {
256             return "";
257         }
258 
259         final StringBuilder serializedEmail = new StringBuilder();
260         final MimeMessage message = wiserMessage.getMimeMessage();
261 
262         // Serialize the headers
263         for (final Enumeration<?> headers = message.getAllHeaders(); headers.hasMoreElements();) {
264             final Header header = (Header) headers.nextElement();
265             serializedEmail.append(header.getName());
266             serializedEmail.append(": ");
267             serializedEmail.append(header.getValue());
268             serializedEmail.append(LINE_SEPARATOR);
269         }
270 
271         // Serialize the body
272         final byte[] messageBody = getMessageBodyBytes(message);
273 
274         serializedEmail.append(LINE_SEPARATOR);
275         serializedEmail.append(messageBody);
276         serializedEmail.append(LINE_SEPARATOR);
277 
278         return serializedEmail.toString();
279     }
280 
281     @BeforeEach
282     public void setUpAbstractEmailTest() {
283         emailOutputDir = new File("target/test-emails");
284         if (!emailOutputDir.exists()) {
285             emailOutputDir.mkdirs();
286         }
287     }
288 
289     protected void stopServer() {
290         if (fakeMailServer != null) {
291             fakeMailServer.stop();
292         }
293     }
294 
295     @AfterEach
296     public void tearDownEmailTest() {
297         // stop the fake email server (if started)
298         if (fakeMailServer != null && !isMailServerStopped(fakeMailServer)) {
299             fakeMailServer.stop();
300             assertTrue(isMailServerStopped(fakeMailServer), "Mail server didn't stop");
301         }
302 
303         fakeMailServer = null;
304     }
305 
306     /**
307      * Validate the message was sent properly
308      *
309      * @param mailServer     reference to the fake mail server
310      * @param strSubject     expected subject
311      * @param fromAdd        expected from address
312      * @param toAdd          list of expected to addresses
313      * @param ccAdd          list of expected cc addresses
314      * @param bccAdd         list of expected bcc addresses
315      * @param boolSaveToFile true will output to file, false doesn't
316      * @return WiserMessage email to check
317      * @throws IOException Exception
318      */
319     protected WiserMessage validateSend(final Wiser mailServer, final String strSubject, final InternetAddress fromAdd, final List<InternetAddress> toAdd,
320             final List<InternetAddress> ccAdd, final List<InternetAddress> bccAdd, final boolean boolSaveToFile) throws IOException {
321         assertTrue(mailServer.getMessages().size() == 1, "mail server doesn't contain expected message");
322         final WiserMessage emailMessage = mailServer.getMessages().get(0);
323 
324         if (boolSaveToFile) {
325             try {
326                 saveEmailToFile(emailMessage);
327             } catch (final MessagingException e) {
328                 final IllegalStateException rethrow = new IllegalStateException("caught MessagingException during saving the email");
329                 rethrow.initCause(e);
330                 throw rethrow;
331             }
332         }
333 
334         try {
335             // get the MimeMessage
336             final MimeMessage mimeMessage = emailMessage.getMimeMessage();
337 
338             // test subject
339             assertEquals(strSubject, mimeMessage.getHeader("Subject", null), "got wrong subject from mail");
340 
341             // test from address
342             assertEquals(fromAdd.toString(), mimeMessage.getHeader("From", null), "got wrong From: address from mail");
343 
344             // test to address
345             assertTrue(toAdd.toString().contains(mimeMessage.getHeader("To", null)), "got wrong To: address from mail");
346 
347             // test cc address
348             if (!ccAdd.isEmpty()) {
349                 assertTrue(ccAdd.toString().contains(mimeMessage.getHeader("Cc", null)), "got wrong Cc: address from mail");
350             }
351 
352             // test bcc address
353             if (!bccAdd.isEmpty()) {
354                 assertTrue(bccAdd.toString().contains(mimeMessage.getHeader("Bcc", null)), "got wrong Bcc: address from mail");
355             }
356         } catch (final MessagingException e) {
357             final IllegalStateException rethrow = new IllegalStateException("caught MessagingException in validateSend()");
358             rethrow.initCause(e);
359             throw rethrow;
360         }
361 
362         return emailMessage;
363     }
364 
365     /**
366      * Validate the message was sent properly
367      *
368      * @param mailServer     reference to the fake mail server
369      * @param strSubject     expected subject
370      * @param content        the expected message content
371      * @param fromAdd        expected from address
372      * @param toAdd          list of expected to addresses
373      * @param ccAdd          list of expected cc addresses
374      * @param bccAdd         list of expected bcc addresses
375      * @param boolSaveToFile true will output to file, false doesn't
376      * @throws IOException Exception
377      */
378     protected void validateSend(final Wiser mailServer, final String strSubject, final Multipart content, final InternetAddress fromAdd,
379             final List<InternetAddress> toAdd, final List<InternetAddress> ccAdd, final List<InternetAddress> bccAdd, final boolean boolSaveToFile)
380             throws IOException {
381         // test other properties
382         final WiserMessage emailMessage = validateSend(mailServer, strSubject, fromAdd, toAdd, ccAdd, bccAdd, boolSaveToFile);
383 
384         // test message content
385 
386         // get sent email content
387         final String strSentContent = content.getContentType();
388         // get received email content (chop off the auto-added \n
389         // and -- (front and end)
390         final String emailMessageBody = getMessageBody(emailMessage);
391         final String strMessageBody = emailMessageBody.substring(AbstractEmailTest.BODY_START_PAD, emailMessageBody.length() - AbstractEmailTest.BODY_END_PAD);
392         assertTrue(strMessageBody.contains(strSentContent), "didn't find expected content type in message body");
393     }
394 
395     /**
396      * Validate the message was sent properly
397      *
398      * @param mailServer     reference to the fake mail server
399      * @param strSubject     expected subject
400      * @param strMessage     the expected message as a string
401      * @param fromAdd        expected from address
402      * @param toAdd          list of expected to addresses
403      * @param ccAdd          list of expected cc addresses
404      * @param bccAdd         list of expected bcc addresses
405      * @param boolSaveToFile true will output to file, false doesn't
406      * @throws IOException Exception
407      */
408     protected void validateSend(final Wiser mailServer, final String strSubject, final String strMessage, final InternetAddress fromAdd,
409             final List<InternetAddress> toAdd, final List<InternetAddress> ccAdd, final List<InternetAddress> bccAdd, final boolean boolSaveToFile)
410             throws IOException {
411         // test other properties
412         final WiserMessage emailMessage = validateSend(mailServer, strSubject, fromAdd, toAdd, ccAdd, bccAdd, true);
413 
414         // test message content
415         assertTrue(getMessageBody(emailMessage).contains(strMessage), "didn't find expected message content in message body");
416     }
417 }