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