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