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 android.content.Context;
20import android.test.AndroidTestCase;
21import android.test.suitebuilder.annotation.SmallTest;
22
23import com.android.email.DBTestHelper;
24import com.android.email.mail.Transport;
25import com.android.email.provider.EmailProvider;
26import com.android.emailcommon.mail.Address;
27import com.android.emailcommon.mail.MessagingException;
28import com.android.emailcommon.provider.Account;
29import com.android.emailcommon.provider.EmailContent.Attachment;
30import com.android.emailcommon.provider.EmailContent.Body;
31import com.android.emailcommon.provider.EmailContent.Message;
32import com.android.emailcommon.provider.HostAuth;
33
34import java.io.IOException;
35import java.net.InetAddress;
36import java.net.UnknownHostException;
37
38/**
39 * This is a series of unit tests for the SMTP Sender class.  These tests must be locally
40 * complete - no server(s) required.
41 *
42 * These tests can be run with the following command:
43 *   runtest -c com.android.email.mail.transport.SmtpSenderUnitTests email
44 */
45@SmallTest
46public class SmtpSenderUnitTests extends AndroidTestCase {
47
48    EmailProvider mProvider;
49    Context mProviderContext;
50    Context mContext;
51    private static final String LOCAL_ADDRESS = "1.2.3.4";
52
53    /* These values are provided by setUp() */
54    private SmtpSender mSender = null;
55
56    /* Simple test string and its base64 equivalent */
57    private final static String TEST_STRING = "Hello, world";
58    private final static String TEST_STRING_BASE64 = "SGVsbG8sIHdvcmxk";
59
60    /**
61     * Setup code.  We generate a lightweight SmtpSender for testing.
62     */
63    @Override
64    protected void setUp() throws Exception {
65        super.setUp();
66        mProviderContext = DBTestHelper.ProviderContextSetupHelper.getProviderContext(
67                getContext());
68        mContext = getContext();
69
70        HostAuth testAuth = new HostAuth();
71        Account testAccount = new Account();
72
73        testAuth.setLogin("user", "password");
74        testAuth.setConnection("smtp", "server", 999);
75        testAccount.mHostAuthSend = testAuth;
76        mSender = (SmtpSender) SmtpSender.newInstance(testAccount, mProviderContext);
77    }
78
79    /**
80     * Confirms simple non-SSL non-TLS login
81     */
82    public void testSimpleLogin() throws Exception {
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 Exception {
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);
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     * Prepare to send a simple message (see setReceiveSimpleMessage)
163     */
164    private Message setupSimpleMessage() {
165        Message message = new Message();
166        message.mTimeStamp = System.currentTimeMillis();
167        message.mFrom = Address.parseAndPack("Jones@Registry.Org");
168        message.mTo = Address.parseAndPack("Smith@Registry.Org");
169        message.mMessageId = "1234567890";
170        return message;
171    }
172
173    /**
174     * Prepare to receive a simple message (see setupSimpleMessage)
175     */
176    private void expectSimpleMessage(MockTransport mockTransport) {
177        mockTransport.expect("MAIL FROM: <Jones@Registry.Org>",
178                "250 2.1.0 <Jones@Registry.Org> sender ok");
179        mockTransport.expect("RCPT TO: <Smith@Registry.Org>",
180                "250 2.1.5 <Smith@Registry.Org> recipient ok");
181        mockTransport.expect("DATA", "354 enter mail, end with . on a line by itself");
182        mockTransport.expect("Date: .*");
183        mockTransport.expect("Message-ID: .*");
184        mockTransport.expect("From: Jones@Registry.Org");
185        mockTransport.expect("To: Smith@Registry.Org");
186        mockTransport.expect("MIME-Version: 1.0");
187    }
188
189    /**
190     * Prepare to send a simple attachment
191     */
192    private Attachment setupSimpleAttachment(Context context, long messageId) {
193        Attachment attachment = new Attachment();
194        attachment.mFileName = "the file.jpg";
195        attachment.mMimeType = "image/jpg";
196        attachment.mSize = 0;
197        attachment.mContentId = null;
198        attachment.mContentUri = "content://com.android.email/1/1";
199        attachment.mMessageKey = messageId;
200        attachment.mLocation = null;
201        attachment.mEncoding = null;
202
203        return attachment;
204    }
205
206    /**
207     * Prepare to receive a simple attachment (note, no multipart support here)
208     */
209    private void expectSimpleAttachment(MockTransport mockTransport, Attachment attachment) {
210        mockTransport.expect("Content-Type: " + attachment.mMimeType + ";");
211        mockTransport.expect(" name=\"" + attachment.mFileName + "\"");
212        mockTransport.expect("Content-Transfer-Encoding: base64");
213        mockTransport.expect("Content-Disposition: attachment;");
214        mockTransport.expect(" filename=\"" + attachment.mFileName + "\";");
215        mockTransport.expect(" size=" + Long.toString(attachment.mSize));
216        mockTransport.expect("");
217        if (attachment.mContentUri != null && attachment.mContentUri.startsWith("file://")) {
218            mockTransport.expect(TEST_STRING_BASE64);
219        }
220    }
221
222    /**
223     * Test:  Recover from a server closing early (or returning an empty string)
224     */
225    public void testEmptyLineResponse() throws Exception {
226        MockTransport mockTransport = openAndInjectMockTransport();
227
228        // Since SmtpSender.sendMessage() does a close then open, we need to preset for the open
229        mockTransport.expectClose();
230
231        // Load up just the bare minimum to expose the error
232        mockTransport.expect(null, "220 MockTransport 2000 Ready To Assist You Peewee");
233        mockTransport.expectLiterally("EHLO [" + LOCAL_ADDRESS + "]", null);
234
235        // Now trigger the transmission
236        // Note, a null message is sufficient here, as we won't even get past open()
237        try {
238            mSender.sendMessage(-1);
239            fail("Should not be able to send with failed open()");
240        } catch (MessagingException me) {
241            // good - expected
242            // TODO maybe expect a particular exception?
243        }
244    }
245
246    /**
247     * Set up a basic MockTransport. open it, and inject it into mStore
248     */
249    private MockTransport openAndInjectMockTransport() throws UnknownHostException {
250        // Create mock transport and inject it into the SmtpSender that's already set up
251        MockTransport mockTransport = new MockTransport();
252        mockTransport.setSecurity(Transport.CONNECTION_SECURITY_NONE, false);
253        mSender.setTransport(mockTransport);
254        mockTransport.setMockLocalAddress(InetAddress.getByName(LOCAL_ADDRESS));
255        return mockTransport;
256    }
257
258    /**
259     * Helper which stuffs the mock with enough strings to satisfy a call to SmtpSender.open()
260     *
261     * @param mockTransport the mock transport we're using
262     * @param capabilities if non-null, comma-separated list of capabilities
263     */
264    private void setupOpen(MockTransport mockTransport, String capabilities) {
265        mockTransport.expect(null, "220 MockTransport 2000 Ready To Assist You Peewee");
266        mockTransport.expect("EHLO .*", "250-10.20.30.40 hello");
267        if (capabilities == null) {
268            mockTransport.expect(null, "250-HELP");
269            mockTransport.expect(null, "250-AUTH LOGIN PLAIN CRAM-MD5");
270            mockTransport.expect(null, "250-SIZE 15728640");
271            mockTransport.expect(null, "250-ENHANCEDSTATUSCODES");
272            mockTransport.expect(null, "250-8BITMIME");
273        } else {
274            for (String capability : capabilities.split(",")) {
275                mockTransport.expect(null, "250-" + capability);
276            }
277        }
278        mockTransport.expect(null, "250+OK");
279        mockTransport.expect("AUTH PLAIN .*", "235 2.7.0 ... authentication succeeded");
280    }
281}
282