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.mail;
18  
19  import static org.easymock.EasyMock.expect;
20  import static org.hamcrest.CoreMatchers.containsString;
21  import static org.junit.Assert.*;
22  import static org.powermock.api.easymock.PowerMock.createMock;
23  import static org.powermock.api.easymock.PowerMock.replay;
24  
25  import java.io.BufferedOutputStream;
26  import java.io.ByteArrayOutputStream;
27  import java.io.File;
28  import java.io.IOException;
29  import java.net.URL;
30  import java.util.Date;
31  import java.util.Enumeration;
32  import java.util.List;
33  
34  import javax.activation.DataHandler;
35  import javax.mail.Header;
36  import javax.mail.MessagingException;
37  import javax.mail.Multipart;
38  import javax.mail.internet.InternetAddress;
39  import javax.mail.internet.MimeMessage;
40  
41  import org.apache.commons.mail.settings.EmailConfiguration;
42  import org.junit.After;
43  import org.junit.Before;
44  import org.subethamail.wiser.Wiser;
45  import org.subethamail.wiser.WiserMessage;
46  
47  /**
48   * Base test case for Email test classes.
49   *
50   * @since 1.0
51   */
52  public abstract class AbstractEmailTest
53  {
54      /** Padding at end of body added by wiser/send */
55      public static final int BODY_END_PAD = 3;
56  
57      /** Padding at start of body added by wiser/send */
58      public static final int BODY_START_PAD = 2;
59  
60      /** Line separator in email messages */
61      private static final String LINE_SEPARATOR = "\r\n";
62  
63      /** default port */
64      private static int mailServerPort = 2500;
65  
66      /** The fake Wiser email server */
67      protected Wiser fakeMailServer;
68  
69      /** Mail server used for testing */
70      protected String strTestMailServer = "localhost";
71  
72      /** From address for the test email */
73      protected String strTestMailFrom = "test_from@apache.org";
74  
75      /** Destination address for the test email */
76      protected String strTestMailTo = "test_to@apache.org";
77  
78      /** Mailserver username (set if needed) */
79      protected String strTestUser = "user";
80  
81      /** Mailserver strTestPasswd (set if needed) */
82      protected String strTestPasswd = "password";
83  
84      /** URL to used to test URL attachments (Must be valid) */
85      protected String strTestURL = EmailConfiguration.TEST_URL;
86  
87      /** Test characters acceptable to email */
88      protected String[] testCharsValid =
89      {
90              " ",
91              "a",
92              "A",
93              "\uc5ec",
94              "0123456789",
95              "012345678901234567890"
96      };
97  
98      /** Test characters not acceptable to email */
99      protected String[] endOfLineCombinations =
100     {
101             "\n",
102             "\r",
103             "\r\n",
104             "\n\r",
105     };
106 
107     /** Array of test strings */
108     protected String[] testCharsNotValid = {"", null};
109 
110     /** Where to save email output **/
111     private File emailOutputDir;
112 
113     /** counter for creating a file name */
114     private static int fileNameCounter;
115 
116     @Before
117     public void setUpAbstractEmailTest()
118     {
119         emailOutputDir = new File("target/test-emails");
120         if (!emailOutputDir.exists())
121         {
122             emailOutputDir.mkdirs();
123         }
124     }
125 
126     @After
127     public void tearDownEmailTest()
128     {
129         //stop the fake email server (if started)
130         if (this.fakeMailServer != null && !isMailServerStopped(fakeMailServer))
131         {
132             this.fakeMailServer.stop();
133             assertTrue("Mail server didn't stop", isMailServerStopped(fakeMailServer));
134         }
135 
136         this.fakeMailServer = null;
137     }
138 
139     /**
140      * Gets the mail server port.
141      * @return the port the server is running on.
142      */
143     protected int getMailServerPort()
144     {
145         return mailServerPort;
146     }
147 
148     /**
149      * Safe a mail to a file using a more or less unique file name.
150      *
151      * @param email email
152      * @throws IOException writing the email failed
153      * @throws MessagingException writing the email failed
154      */
155     protected void saveEmailToFile(final WiserMessage email) throws IOException, MessagingException
156     {
157         final int currCounter = fileNameCounter++ % 10;
158         final String emailFileName = "email" + new Date().getTime() + "-" + currCounter + ".eml";
159         final File emailFile = new File(emailOutputDir, emailFileName);
160         EmailUtils.writeMimeMessage(emailFile, email.getMimeMessage() );
161     }
162 
163     /**
164      * @param intMsgNo the message to retrieve
165      * @return message as string
166      */
167     public String getMessageAsString(final int intMsgNo)
168     {
169         final List<?> receivedMessages = fakeMailServer.getMessages();
170         assertTrue("mail server didn't get enough messages", receivedMessages.size() >= intMsgNo);
171 
172         final WiserMessage emailMessage = (WiserMessage) receivedMessages.get(intMsgNo);
173 
174         if (emailMessage != null)
175         {
176             try
177             {
178                 return serializeEmailMessage(emailMessage);
179             }
180             catch (final Exception e)
181             {
182                 // ignore, since the test will fail on an empty string return
183             }
184         }
185         fail("Message not found");
186         return "";
187     }
188 
189     /**
190      * Initializes the stub mail server. Fails if the server cannot be proven
191      * to have started. If the server is already started, this method returns
192      * without changing the state of the server.
193      */
194     public void getMailServer()
195     {
196         if (this.fakeMailServer == null || isMailServerStopped(fakeMailServer))
197         {
198             mailServerPort++;
199 
200             this.fakeMailServer = new Wiser();
201             this.fakeMailServer.setPort(getMailServerPort());
202             this.fakeMailServer.start();
203 
204             assertFalse("fake mail server didn't start", isMailServerStopped(fakeMailServer));
205 
206             final Date dtStartWait = new Date();
207             while (isMailServerStopped(fakeMailServer))
208             {
209                 // test for connected
210                 if (this.fakeMailServer != null
211                     && !isMailServerStopped(fakeMailServer))
212                 {
213                     break;
214                 }
215 
216                 // test for timeout
217                 if (dtStartWait.getTime() + EmailConfiguration.TIME_OUT
218                     <= new Date().getTime())
219                 {
220                     fail("Mail server failed to start");
221                 }
222             }
223         }
224     }
225 
226     /**
227      * Validate the message was sent properly
228      * @param mailServer reference to the fake mail server
229      * @param strSubject expected subject
230      * @param fromAdd expected from address
231      * @param toAdd list of expected to addresses
232      * @param ccAdd list of expected cc addresses
233      * @param bccAdd list of expected bcc addresses
234      * @param boolSaveToFile true will output to file, false doesnt
235      * @return WiserMessage email to check
236      * @throws IOException Exception
237      */
238     protected WiserMessage validateSend(
239         final Wiser mailServer,
240         final String strSubject,
241         final InternetAddress fromAdd,
242         final List<InternetAddress> toAdd,
243         final List<InternetAddress> ccAdd,
244         final List<InternetAddress> bccAdd,
245         final boolean boolSaveToFile)
246         throws IOException
247     {
248         assertTrue("mail server doesn't contain expected message",
249                 mailServer.getMessages().size() == 1);
250         final WiserMessage emailMessage = mailServer.getMessages().get(0);
251 
252         if (boolSaveToFile)
253         {
254             try
255             {
256                 this.saveEmailToFile(emailMessage);
257             }
258             catch(final MessagingException me)
259             {
260                 final IllegalStateException ise =
261                     new IllegalStateException("caught MessagingException during saving the email");
262                 ise.initCause(me);
263                 throw ise;
264             }
265         }
266 
267         try
268         {
269             // get the MimeMessage
270             final MimeMessage mimeMessage = emailMessage.getMimeMessage();
271 
272             // test subject
273             assertEquals("got wrong subject from mail",
274                     strSubject, mimeMessage.getHeader("Subject", null));
275 
276             //test from address
277             assertEquals("got wrong From: address from mail",
278                     fromAdd.toString(), mimeMessage.getHeader("From", null));
279 
280             //test to address
281             assertTrue("got wrong To: address from mail",
282                     toAdd.toString().contains(mimeMessage.getHeader("To", null)));
283 
284             //test cc address
285             if (ccAdd.size() > 0)
286             {
287                 assertTrue("got wrong Cc: address from mail",
288                     ccAdd.toString().contains(mimeMessage.getHeader("Cc", null)));
289             }
290 
291             //test bcc address
292             if (bccAdd.size() > 0)
293             {
294                 assertTrue("got wrong Bcc: address from mail",
295                     bccAdd.toString().contains(mimeMessage.getHeader("Bcc", null)));
296             }
297         }
298         catch (final MessagingException me)
299         {
300             final IllegalStateException ise =
301                 new IllegalStateException("caught MessagingException in validateSend()");
302             ise.initCause(me);
303             throw ise;
304         }
305 
306         return emailMessage;
307     }
308 
309     /**
310      * Validate the message was sent properly
311      * @param mailServer reference to the fake mail server
312      * @param strSubject expected subject
313      * @param content the expected message content
314      * @param fromAdd expected from address
315      * @param toAdd list of expected to addresses
316      * @param ccAdd list of expected cc addresses
317      * @param bccAdd list of expected bcc addresses
318      * @param boolSaveToFile true will output to file, false doesnt
319      * @throws IOException Exception
320      */
321     protected void validateSend(
322         final Wiser mailServer,
323         final String strSubject,
324         final Multipart content,
325         final InternetAddress fromAdd,
326         final List<InternetAddress> toAdd,
327         final List<InternetAddress> ccAdd,
328         final List<InternetAddress> bccAdd,
329         final boolean boolSaveToFile)
330         throws IOException
331     {
332         // test other properties
333         final WiserMessage emailMessage = this.validateSend(
334             mailServer,
335             strSubject,
336             fromAdd,
337             toAdd,
338             ccAdd,
339             bccAdd,
340             boolSaveToFile);
341 
342         // test message content
343 
344         // get sent email content
345         final String strSentContent =
346             content.getContentType();
347         // get received email content (chop off the auto-added \n
348         // and -- (front and end)
349         final String emailMessageBody = getMessageBody(emailMessage);
350         final String strMessageBody =
351             emailMessageBody.substring(AbstractEmailTest.BODY_START_PAD,
352                                        emailMessageBody.length()
353                                        - AbstractEmailTest.BODY_END_PAD);
354         assertTrue("didn't find expected content type in message body",
355                 strMessageBody.contains(strSentContent));
356     }
357 
358     /**
359      * Validate the message was sent properly
360      * @param mailServer reference to the fake mail server
361      * @param strSubject expected subject
362      * @param strMessage the expected message as a string
363      * @param fromAdd expected from address
364      * @param toAdd list of expected to addresses
365      * @param ccAdd list of expected cc addresses
366      * @param bccAdd list of expected bcc addresses
367      * @param boolSaveToFile true will output to file, false doesnt
368      * @throws IOException Exception
369      */
370     protected void validateSend(
371         final Wiser mailServer,
372         final String strSubject,
373         final String strMessage,
374         final InternetAddress fromAdd,
375         final List<InternetAddress> toAdd,
376         final List<InternetAddress> ccAdd,
377         final List<InternetAddress> bccAdd,
378         final boolean boolSaveToFile)
379         throws IOException
380     {
381         // test other properties
382         final WiserMessage emailMessage = this.validateSend(
383             mailServer,
384             strSubject,
385             fromAdd,
386             toAdd,
387             ccAdd,
388             bccAdd,
389             true);
390 
391         // test message content
392         assertThat("didn't find expected message content in message body",
393                 getMessageBody(emailMessage), containsString(strMessage));
394     }
395 
396     /**
397      * Serializes the {@link MimeMessage} from the <code>WiserMessage</code>
398      * passed in. The headers are serialized first followed by the message
399      * body.
400      *
401      * @param wiserMessage The <code>WiserMessage</code> to serialize.
402      * @return The string format of the message.
403      * @throws MessagingException
404      * @throws IOException
405      *             Thrown while serializing the body from
406      *             {@link DataHandler#writeTo(java.io.OutputStream)}.
407      * @throws MessagingException
408      *             Thrown while getting the body content from
409      *             {@link MimeMessage#getDataHandler()}
410      * @since 1.1
411      */
412     private String serializeEmailMessage(final WiserMessage wiserMessage)
413             throws MessagingException, IOException
414     {
415         if (wiserMessage == null)
416         {
417             return "";
418         }
419 
420         final StringBuffer serializedEmail = new StringBuffer();
421         final MimeMessage message = wiserMessage.getMimeMessage();
422 
423         // Serialize the headers
424         for (final Enumeration<?> headers = message.getAllHeaders(); headers
425                 .hasMoreElements();)
426         {
427             final Header header = (Header) headers.nextElement();
428             serializedEmail.append(header.getName());
429             serializedEmail.append(": ");
430             serializedEmail.append(header.getValue());
431             serializedEmail.append(LINE_SEPARATOR);
432         }
433 
434         // Serialize the body
435         final byte[] messageBody = getMessageBodyBytes(message);
436 
437         serializedEmail.append(LINE_SEPARATOR);
438         serializedEmail.append(messageBody);
439         serializedEmail.append(LINE_SEPARATOR);
440 
441         return serializedEmail.toString();
442     }
443 
444     /**
445      * Returns a string representation of the message body. If the message body
446      * cannot be read, an empty string is returned.
447      *
448      * @param wiserMessage The wiser message from which to extract the message body
449      * @return The string representation of the message body
450      * @throws IOException
451      *             Thrown while serializing the body from
452      *             {@link DataHandler#writeTo(java.io.OutputStream)}.
453      * @since 1.1
454      */
455     private String getMessageBody(final WiserMessage wiserMessage)
456             throws IOException
457     {
458         if (wiserMessage == null)
459         {
460             return "";
461         }
462 
463         byte[] messageBody = null;
464 
465         try
466         {
467             final MimeMessage message = wiserMessage.getMimeMessage();
468             messageBody = getMessageBodyBytes(message);
469         }
470         catch (final MessagingException me)
471         {
472             // Thrown while getting the body content from
473             // {@link MimeMessage#getDataHandler()}
474             final IllegalStateException ise =
475                 new IllegalStateException("couldn't process MimeMessage from WiserMessage in getMessageBody()");
476             ise.initCause(me);
477             throw ise;
478         }
479 
480         return messageBody != null ? new String(messageBody).intern() : "";
481     }
482 
483     /**
484      * Gets the byte making up the body of the message.
485      *
486      * @param mimeMessage
487      *            The mime message from which to extract the body.
488      * @return A byte array representing the message body
489      * @throws IOException
490      *             Thrown while serializing the body from
491      *             {@link DataHandler#writeTo(java.io.OutputStream)}.
492      * @throws MessagingException
493      *             Thrown while getting the body content from
494      *             {@link MimeMessage#getDataHandler()}
495      * @since 1.1
496      */
497     private byte[] getMessageBodyBytes(final MimeMessage mimeMessage)
498             throws IOException, MessagingException
499     {
500         final DataHandler dataHandler = mimeMessage.getDataHandler();
501         final ByteArrayOutputStream byteArrayOutStream = new ByteArrayOutputStream();
502         final BufferedOutputStream buffOs = new BufferedOutputStream(
503                 byteArrayOutStream);
504         dataHandler.writeTo(buffOs);
505         buffOs.flush();
506 
507         return byteArrayOutStream.toByteArray();
508     }
509 
510     /**
511      * Checks if an email server is running at the address stored in the
512      * <code>fakeMailServer</code>.
513      *
514      * @param fakeMailServer
515      *            The server from which the address is picked up.
516      * @return <code>true</code> if the server claims to be running
517      * @since 1.1
518      */
519     protected boolean isMailServerStopped(final Wiser fakeMailServer) {
520         return !fakeMailServer.getServer().isRunning();
521     }
522     
523     /**
524      * Create a mocked URL object which always throws an IOException
525      * when the openStream() method is called.
526      * <p>
527      * Several ISPs do resolve invalid URLs like {@code http://example.invalid}
528      * to some error page causing tests to fail otherwise.
529      *
530      * @return an invalid URL
531      */
532     protected URL createInvalidURL() throws Exception {
533         final URL url = createMock(URL.class);
534         expect(url.openStream()).andThrow(new IOException());
535         replay(url);
536         
537         return url;
538     }
539 }