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