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