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