Home | History | Annotate | Download | only in transport
      1 /*
      2  * Copyright (C) 2008 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.email.mail.transport;
     18 
     19 import com.android.email.mail.Address;
     20 import com.android.email.mail.MessagingException;
     21 import com.android.email.mail.Transport;
     22 import com.android.email.provider.EmailProvider;
     23 import com.android.email.provider.EmailContent.Attachment;
     24 import com.android.email.provider.EmailContent.Body;
     25 import com.android.email.provider.EmailContent.Message;
     26 
     27 import org.apache.commons.io.IOUtils;
     28 
     29 import android.content.Context;
     30 import android.test.ProviderTestCase2;
     31 import android.test.suitebuilder.annotation.SmallTest;
     32 
     33 import java.io.ByteArrayInputStream;
     34 import java.io.File;
     35 import java.io.FileOutputStream;
     36 import java.io.IOException;
     37 import java.io.InputStream;
     38 import java.io.OutputStream;
     39 import java.net.InetAddress;
     40 import java.net.UnknownHostException;
     41 import java.util.regex.Pattern;
     42 
     43 /**
     44  * This is a series of unit tests for the SMTP Sender class.  These tests must be locally
     45  * complete - no server(s) required.
     46  *
     47  * These tests can be run with the following command:
     48  *   runtest -c com.android.email.mail.transport.SmtpSenderUnitTests email
     49  */
     50 @SmallTest
     51 public class SmtpSenderUnitTests extends ProviderTestCase2<EmailProvider> {
     52 
     53     EmailProvider mProvider;
     54     Context mProviderContext;
     55     Context mContext;
     56     private static final String LOCAL_ADDRESS = "1.2.3.4";
     57 
     58     /* These values are provided by setUp() */
     59     private SmtpSender mSender = null;
     60 
     61     /* Simple test string and its base64 equivalent */
     62     private final static String TEST_STRING = "Hello, world";
     63     private final static String TEST_STRING_BASE64 = "SGVsbG8sIHdvcmxk";
     64 
     65     public SmtpSenderUnitTests() {
     66         super(EmailProvider.class, EmailProvider.EMAIL_AUTHORITY);
     67     }
     68 
     69     /**
     70      * Setup code.  We generate a lightweight SmtpSender for testing.
     71      */
     72     @Override
     73     protected void setUp() throws Exception {
     74         super.setUp();
     75         mProviderContext = getMockContext();
     76         mContext = getContext();
     77 
     78         // These are needed so we can get at the inner classes
     79         mSender = (SmtpSender) SmtpSender.newInstance(mProviderContext,
     80                 "smtp://user:password@server:999");
     81     }
     82 
     83     /**
     84      * Confirms simple non-SSL non-TLS login
     85      */
     86     public void testSimpleLogin() throws Exception {
     87 
     88         MockTransport mockTransport = openAndInjectMockTransport();
     89 
     90         // try to open it
     91         setupOpen(mockTransport, null);
     92         mSender.open();
     93     }
     94 
     95     /**
     96      * TODO: Test with SSL negotiation (faked)
     97      * TODO: Test with SSL required but not supported
     98      * TODO: Test with TLS negotiation (faked)
     99      * TODO: Test with TLS required but not supported
    100      * TODO: Test other capabilities.
    101      * TODO: Test AUTH LOGIN
    102      */
    103 
    104     /**
    105      * Test:  Open and send a single message (sunny day)
    106      */
    107     public void testSendMessageWithBody() throws Exception {
    108         MockTransport mockTransport = openAndInjectMockTransport();
    109 
    110         // Since SmtpSender.sendMessage() does a close then open, we need to preset for the open
    111         mockTransport.expectClose();
    112         setupOpen(mockTransport, null);
    113 
    114         Message message = setupSimpleMessage();
    115         message.save(mProviderContext);
    116 
    117         Body body = new Body();
    118         body.mMessageKey = message.mId;
    119         body.mTextContent = TEST_STRING;
    120         body.save(mProviderContext);
    121 
    122         // prepare for the message traffic we'll see
    123         // TODO The test is a bit fragile, as we are order-dependent (and headers are not)
    124         expectSimpleMessage(mockTransport);
    125         mockTransport.expect("Content-Type: text/plain; charset=utf-8");
    126         mockTransport.expect("Content-Transfer-Encoding: base64");
    127         mockTransport.expect("");
    128         mockTransport.expect(TEST_STRING_BASE64);
    129         mockTransport.expect("\r\n\\.", "250 2.0.0 kv2f1a00C02Rf8w3Vv mail accepted for delivery");
    130 
    131         // Now trigger the transmission
    132         mSender.sendMessage(message.mId);
    133     }
    134 
    135     /**
    136      * Test:  Open and send a single message with an empty attachment (no file) (sunny day)
    137      */
    138     public void testSendMessageWithEmptyAttachment() throws MessagingException, IOException {
    139         MockTransport mockTransport = openAndInjectMockTransport();
    140 
    141         // Since SmtpSender.sendMessage() does a close then open, we need to preset for the open
    142         mockTransport.expectClose();
    143         setupOpen(mockTransport, null);
    144 
    145         Message message = setupSimpleMessage();
    146         message.save(mProviderContext);
    147 
    148         // Creates an attachment with a bogus file (so we get headers only)
    149         Attachment attachment = setupSimpleAttachment(mProviderContext, message.mId, false);
    150         attachment.save(mProviderContext);
    151 
    152         expectSimpleMessage(mockTransport);
    153         mockTransport.expect("Content-Type: multipart/mixed; boundary=\".*");
    154         mockTransport.expect("");
    155         mockTransport.expect("----.*");
    156         expectSimpleAttachment(mockTransport, attachment);
    157         mockTransport.expect("");
    158         mockTransport.expect("----.*--");
    159         mockTransport.expect("\r\n\\.", "250 2.0.0 kv2f1a00C02Rf8w3Vv mail accepted for delivery");
    160 
    161         // Now trigger the transmission
    162         mSender.sendMessage(message.mId);
    163     }
    164 
    165     /**
    166      * Test:  Open and send a single message with an attachment (sunny day)
    167      */
    168     public void testSendMessageWithAttachment() throws MessagingException, IOException {
    169         MockTransport mockTransport = openAndInjectMockTransport();
    170 
    171         // Since SmtpSender.sendMessage() does a close then open, we need to preset for the open
    172         mockTransport.expectClose();
    173         setupOpen(mockTransport, null);
    174 
    175         Message message = setupSimpleMessage();
    176         message.save(mProviderContext);
    177 
    178         // Creates an attachment with a real file
    179         Attachment attachment = setupSimpleAttachment(mProviderContext, message.mId, true);
    180         attachment.save(mProviderContext);
    181 
    182         expectSimpleMessage(mockTransport);
    183         mockTransport.expect("Content-Type: multipart/mixed; boundary=\".*");
    184         mockTransport.expect("");
    185         mockTransport.expect("----.*");
    186         expectSimpleAttachment(mockTransport, attachment);
    187         mockTransport.expect("");
    188         mockTransport.expect("----.*--");
    189         mockTransport.expect("\r\n\\.", "250 2.0.0 kv2f1a00C02Rf8w3Vv mail accepted for delivery");
    190 
    191         // Now trigger the transmission
    192         mSender.sendMessage(message.mId);
    193     }
    194 
    195     /**
    196      * Test:  Open and send a single message with two attachments
    197      */
    198     public void testSendMessageWithTwoAttachments() throws MessagingException, IOException {
    199         MockTransport mockTransport = openAndInjectMockTransport();
    200 
    201         // Since SmtpSender.sendMessage() does a close then open, we need to preset for the open
    202         mockTransport.expectClose();
    203         setupOpen(mockTransport, null);
    204 
    205         Message message = setupSimpleMessage();
    206         message.save(mProviderContext);
    207 
    208         // Creates an attachment with a real file
    209         Attachment attachment = setupSimpleAttachment(mProviderContext, message.mId, true);
    210         attachment.save(mProviderContext);
    211 
    212         // Creates an attachment with a real file
    213         Attachment attachment2 = setupSimpleAttachment(mProviderContext, message.mId, true);
    214         attachment2.save(mProviderContext);
    215 
    216         expectSimpleMessage(mockTransport);
    217         mockTransport.expect("Content-Type: multipart/mixed; boundary=\".*");
    218         mockTransport.expect("");
    219         mockTransport.expect("----.*");
    220         expectSimpleAttachment(mockTransport, attachment);
    221         mockTransport.expect("");
    222         mockTransport.expect("----.*");
    223         expectSimpleAttachment(mockTransport, attachment2);
    224         mockTransport.expect("");
    225         mockTransport.expect("----.*--");
    226         mockTransport.expect("\r\n\\.", "250 2.0.0 kv2f1a00C02Rf8w3Vv mail accepted for delivery");
    227 
    228         // Now trigger the transmission
    229         mSender.sendMessage(message.mId);
    230     }
    231 
    232     /**
    233      * Test:  Open and send a single message with body & attachment (sunny day)
    234      */
    235     public void testSendMessageWithBodyAndAttachment() throws MessagingException, IOException {
    236         MockTransport mockTransport = openAndInjectMockTransport();
    237 
    238         // Since SmtpSender.sendMessage() does a close then open, we need to preset for the open
    239         mockTransport.expectClose();
    240         setupOpen(mockTransport, null);
    241 
    242         Message message = setupSimpleMessage();
    243         message.save(mProviderContext);
    244 
    245         Body body = new Body();
    246         body.mMessageKey = message.mId;
    247         body.mTextContent = TEST_STRING;
    248         body.save(mProviderContext);
    249 
    250         Attachment attachment = setupSimpleAttachment(mProviderContext, message.mId, true);
    251         attachment.save(mProviderContext);
    252 
    253         // prepare for the message traffic we'll see
    254         expectSimpleMessage(mockTransport);
    255         mockTransport.expect("Content-Type: multipart/mixed; boundary=\".*");
    256         mockTransport.expect("");
    257         mockTransport.expect("----.*");
    258         mockTransport.expect("Content-Type: text/plain; charset=utf-8");
    259         mockTransport.expect("Content-Transfer-Encoding: base64");
    260         mockTransport.expect("");
    261         mockTransport.expect(TEST_STRING_BASE64);
    262         mockTransport.expect("----.*");
    263         expectSimpleAttachment(mockTransport, attachment);
    264         mockTransport.expect("");
    265         mockTransport.expect("----.*--");
    266         mockTransport.expect("\r\n\\.", "250 2.0.0 kv2f1a00C02Rf8w3Vv mail accepted for delivery");
    267 
    268         // Now trigger the transmission
    269         mSender.sendMessage(message.mId);
    270     }
    271 
    272     /**
    273      * Prepare to send a simple message (see setReceiveSimpleMessage)
    274      */
    275     private Message setupSimpleMessage() {
    276         Message message = new Message();
    277         message.mTimeStamp = System.currentTimeMillis();
    278         message.mFrom = Address.parseAndPack("Jones (at) Registry.Org");
    279         message.mTo = Address.parseAndPack("Smith (at) Registry.Org");
    280         message.mMessageId = "1234567890";
    281         return message;
    282     }
    283 
    284     /**
    285      * Prepare to receive a simple message (see setupSimpleMessage)
    286      */
    287     private void expectSimpleMessage(MockTransport mockTransport) {
    288         mockTransport.expect("MAIL FROM: <Jones (at) Registry.Org>",
    289                 "250 2.1.0 <Jones (at) Registry.Org> sender ok");
    290         mockTransport.expect("RCPT TO: <Smith (at) Registry.Org>",
    291                 "250 2.1.5 <Smith (at) Registry.Org> recipient ok");
    292         mockTransport.expect("DATA", "354 enter mail, end with . on a line by itself");
    293         mockTransport.expect("Date: .*");
    294         mockTransport.expect("Message-ID: .*");
    295         mockTransport.expect("From: Jones (at) Registry.Org");
    296         mockTransport.expect("To: Smith (at) Registry.Org");
    297         mockTransport.expect("MIME-Version: 1.0");
    298     }
    299 
    300     /**
    301      * Prepare to send a simple attachment
    302      */
    303     private Attachment setupSimpleAttachment(Context context, long messageId, boolean withBody)
    304             throws IOException {
    305         Attachment attachment = new Attachment();
    306         attachment.mFileName = "the file.jpg";
    307         attachment.mMimeType = "image/jpg";
    308         attachment.mSize = 0;
    309         attachment.mContentId = null;
    310         attachment.mContentUri = "content://com.android.email/1/1";
    311         attachment.mMessageKey = messageId;
    312         attachment.mLocation = null;
    313         attachment.mEncoding = null;
    314 
    315         if (withBody) {
    316             // Is there an easier way to set up a temp file?
    317             InputStream inStream = new ByteArrayInputStream(TEST_STRING.getBytes());
    318             File cacheDir = context.getCacheDir();
    319             File tmpFile = File.createTempFile("setupSimpleAttachment", "tmp", cacheDir);
    320             OutputStream outStream = new FileOutputStream(tmpFile);
    321 
    322             IOUtils.copy(inStream, outStream);
    323             attachment.mContentUri = "file://" + tmpFile.getAbsolutePath();
    324         }
    325 
    326         return attachment;
    327     }
    328 
    329     /**
    330      * Prepare to receive a simple attachment (note, no multipart support here)
    331      */
    332     private void expectSimpleAttachment(MockTransport mockTransport, Attachment attachment) {
    333         mockTransport.expect("Content-Type: " + attachment.mMimeType + ";");
    334         mockTransport.expect(" name=\"" + attachment.mFileName + "\"");
    335         mockTransport.expect("Content-Transfer-Encoding: base64");
    336         mockTransport.expect("Content-Disposition: attachment;");
    337         mockTransport.expect(" filename=\"" + attachment.mFileName + "\";");
    338         mockTransport.expect(" size=" + Long.toString(attachment.mSize));
    339         mockTransport.expect("");
    340         if (attachment.mContentUri != null && attachment.mContentUri.startsWith("file://")) {
    341             mockTransport.expect(TEST_STRING_BASE64);
    342         }
    343     }
    344 
    345     /**
    346      * Test:  Recover from a server closing early (or returning an empty string)
    347      */
    348     public void testEmptyLineResponse() throws Exception {
    349         MockTransport mockTransport = openAndInjectMockTransport();
    350 
    351         // Since SmtpSender.sendMessage() does a close then open, we need to preset for the open
    352         mockTransport.expectClose();
    353 
    354         // Load up just the bare minimum to expose the error
    355         mockTransport.expect(null, "220 MockTransport 2000 Ready To Assist You Peewee");
    356         mockTransport.expect("EHLO " + Pattern.quote(LOCAL_ADDRESS), "");
    357 
    358         // Now trigger the transmission
    359         // Note, a null message is sufficient here, as we won't even get past open()
    360         try {
    361             mSender.sendMessage(-1);
    362             fail("Should not be able to send with failed open()");
    363         } catch (MessagingException me) {
    364             // good - expected
    365             // TODO maybe expect a particular exception?
    366         }
    367     }
    368 
    369     /**
    370      * Set up a basic MockTransport. open it, and inject it into mStore
    371      */
    372     private MockTransport openAndInjectMockTransport() throws UnknownHostException {
    373         // Create mock transport and inject it into the SmtpSender that's already set up
    374         MockTransport mockTransport = new MockTransport();
    375         mockTransport.setSecurity(Transport.CONNECTION_SECURITY_NONE, false);
    376         mSender.setTransport(mockTransport);
    377         mockTransport.setMockLocalAddress(InetAddress.getByName(LOCAL_ADDRESS));
    378         return mockTransport;
    379     }
    380 
    381     /**
    382      * Helper which stuffs the mock with enough strings to satisfy a call to SmtpSender.open()
    383      *
    384      * @param mockTransport the mock transport we're using
    385      * @param capabilities if non-null, comma-separated list of capabilities
    386      */
    387     private void setupOpen(MockTransport mockTransport, String capabilities) {
    388         mockTransport.expect(null, "220 MockTransport 2000 Ready To Assist You Peewee");
    389         mockTransport.expect("EHLO .*", "250-10.20.30.40 hello");
    390         if (capabilities == null) {
    391             mockTransport.expect(null, "250-HELP");
    392             mockTransport.expect(null, "250-AUTH LOGIN PLAIN CRAM-MD5");
    393             mockTransport.expect(null, "250-SIZE 15728640");
    394             mockTransport.expect(null, "250-ENHANCEDSTATUSCODES");
    395             mockTransport.expect(null, "250-8BITMIME");
    396         } else {
    397             for (String capability : capabilities.split(",")) {
    398                 mockTransport.expect(null, "250-" + capability);
    399             }
    400         }
    401         mockTransport.expect(null, "250+OK");
    402         mockTransport.expect("AUTH PLAIN .*", "235 2.7.0 ... authentication succeeded");
    403     }
    404 }
    405