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 android.content.Context; 20 import android.test.AndroidTestCase; 21 import android.test.suitebuilder.annotation.SmallTest; 22 23 import com.android.email.DBTestHelper; 24 import com.android.email.mail.Transport; 25 import com.android.email.provider.EmailProvider; 26 import com.android.emailcommon.mail.Address; 27 import com.android.emailcommon.mail.MessagingException; 28 import com.android.emailcommon.provider.Account; 29 import com.android.emailcommon.provider.EmailContent.Attachment; 30 import com.android.emailcommon.provider.EmailContent.Body; 31 import com.android.emailcommon.provider.EmailContent.Message; 32 import com.android.emailcommon.provider.HostAuth; 33 34 import java.io.IOException; 35 import java.net.InetAddress; 36 import 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 46 public 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 (at) Registry.Org"); 168 message.mTo = Address.parseAndPack("Smith (at) 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 (at) Registry.Org>", 178 "250 2.1.0 <Jones (at) Registry.Org> sender ok"); 179 mockTransport.expect("RCPT TO: <Smith (at) Registry.Org>", 180 "250 2.1.5 <Smith (at) 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 (at) Registry.Org"); 185 mockTransport.expect("To: Smith (at) 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