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 import android.test.suitebuilder.annotation.Suppress; 23 24 import com.android.email.DBTestHelper; 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 @Suppress 46 @SmallTest 47 public class SmtpSenderUnitTests extends AndroidTestCase { 48 49 EmailProvider mProvider; 50 Context mProviderContext; 51 Context mContext; 52 HostAuth mHostAuth; 53 private static final String LOCAL_ADDRESS = "1.2.3.4"; 54 55 /* These values are provided by setUp() */ 56 private SmtpSender mSender = null; 57 58 /* Simple test string and its base64 equivalent */ 59 private final static String TEST_STRING = "Hello, world"; 60 private final static String TEST_STRING_BASE64 = "SGVsbG8sIHdvcmxk"; 61 62 /** 63 * Setup code. We generate a lightweight SmtpSender for testing. 64 */ 65 @Override 66 protected void setUp() throws Exception { 67 super.setUp(); 68 mProviderContext = DBTestHelper.ProviderContextSetupHelper.getProviderContext( 69 getContext()); 70 mContext = getContext(); 71 72 mHostAuth = new HostAuth(); 73 Account testAccount = new Account(); 74 75 mHostAuth.setLogin("user", "password"); 76 mHostAuth.setConnection("smtp", "server", 999); 77 testAccount.mHostAuthSend = mHostAuth; 78 mSender = (SmtpSender) SmtpSender.newInstance(testAccount, mProviderContext); 79 } 80 81 /** 82 * Confirms simple non-SSL non-TLS login 83 */ 84 public void testSimpleLogin() throws Exception { 85 86 MockTransport mockTransport = openAndInjectMockTransport(); 87 88 // try to open it 89 setupOpen(mockTransport, null); 90 mSender.open(); 91 } 92 93 /** 94 * TODO: Test with SSL negotiation (faked) 95 * TODO: Test with SSL required but not supported 96 * TODO: Test with TLS negotiation (faked) 97 * TODO: Test with TLS required but not supported 98 * TODO: Test other capabilities. 99 * TODO: Test AUTH LOGIN 100 */ 101 102 /** 103 * Test: Open and send a single message (sunny day) 104 */ 105 public void testSendMessageWithBody() throws Exception { 106 MockTransport mockTransport = openAndInjectMockTransport(); 107 108 // Since SmtpSender.sendMessage() does a close then open, we need to preset for the open 109 mockTransport.expectClose(); 110 setupOpen(mockTransport, null); 111 112 Message message = setupSimpleMessage(); 113 message.save(mProviderContext); 114 115 Body body = new Body(); 116 body.mMessageKey = message.mId; 117 body.mTextContent = TEST_STRING; 118 body.save(mProviderContext); 119 120 // prepare for the message traffic we'll see 121 // TODO The test is a bit fragile, as we are order-dependent (and headers are not) 122 expectSimpleMessage(mockTransport); 123 mockTransport.expect("Content-Type: text/plain; charset=utf-8"); 124 mockTransport.expect("Content-Transfer-Encoding: base64"); 125 mockTransport.expect(""); 126 mockTransport.expect(TEST_STRING_BASE64); 127 mockTransport.expect("\r\n\\.", "250 2.0.0 kv2f1a00C02Rf8w3Vv mail accepted for delivery"); 128 129 // Now trigger the transmission 130 mSender.sendMessage(message.mId); 131 } 132 133 /** 134 * Test: Open and send a single message with an empty attachment (no file) (sunny day) 135 */ 136 public void testSendMessageWithEmptyAttachment() throws MessagingException, IOException { 137 MockTransport mockTransport = openAndInjectMockTransport(); 138 139 // Since SmtpSender.sendMessage() does a close then open, we need to preset for the open 140 mockTransport.expectClose(); 141 setupOpen(mockTransport, null); 142 143 Message message = setupSimpleMessage(); 144 message.save(mProviderContext); 145 146 // Creates an attachment with a bogus file (so we get headers only) 147 Attachment attachment = setupSimpleAttachment(mProviderContext, message.mId); 148 attachment.save(mProviderContext); 149 150 expectSimpleMessage(mockTransport); 151 mockTransport.expect("Content-Type: multipart/mixed; boundary=\".*"); 152 mockTransport.expect(""); 153 mockTransport.expect("----.*"); 154 expectSimpleAttachment(mockTransport, attachment); 155 mockTransport.expect(""); 156 mockTransport.expect("----.*--"); 157 mockTransport.expect("\r\n\\.", "250 2.0.0 kv2f1a00C02Rf8w3Vv mail accepted for delivery"); 158 159 // Now trigger the transmission 160 mSender.sendMessage(message.mId); 161 } 162 163 /** 164 * Prepare to send a simple message (see setReceiveSimpleMessage) 165 */ 166 private Message setupSimpleMessage() { 167 Message message = new Message(); 168 message.mTimeStamp = System.currentTimeMillis(); 169 message.mFrom = Address.parseToHeader("Jones (at) Registry.Org"); 170 message.mTo = Address.parseToHeader("Smith (at) Registry.Org"); 171 message.mMessageId = "1234567890"; 172 return message; 173 } 174 175 /** 176 * Prepare to receive a simple message (see setupSimpleMessage) 177 */ 178 private void expectSimpleMessage(MockTransport mockTransport) { 179 mockTransport.expect("MAIL FROM:<Jones (at) Registry.Org>", 180 "250 2.1.0 <Jones (at) Registry.Org> sender ok"); 181 mockTransport.expect("RCPT TO:<Smith (at) Registry.Org>", 182 "250 2.1.5 <Smith (at) Registry.Org> recipient ok"); 183 mockTransport.expect("DATA", "354 enter mail, end with . on a line by itself"); 184 mockTransport.expect("Date: .*"); 185 mockTransport.expect("Message-ID: .*"); 186 mockTransport.expect("From: Jones (at) Registry.Org"); 187 mockTransport.expect("To: Smith (at) Registry.Org"); 188 mockTransport.expect("MIME-Version: 1.0"); 189 } 190 191 /** 192 * Prepare to send a simple attachment 193 */ 194 private Attachment setupSimpleAttachment(Context context, long messageId) { 195 Attachment attachment = new Attachment(); 196 attachment.mFileName = "the file.jpg"; 197 attachment.mMimeType = "image/jpg"; 198 attachment.mSize = 0; 199 attachment.mContentId = null; 200 attachment.setContentUri("content://com.android.email/1/1"); 201 attachment.mMessageKey = messageId; 202 attachment.mLocation = null; 203 attachment.mEncoding = null; 204 205 return attachment; 206 } 207 208 /** 209 * Prepare to receive a simple attachment (note, no multipart support here) 210 */ 211 private void expectSimpleAttachment(MockTransport mockTransport, Attachment attachment) { 212 mockTransport.expect("Content-Type: " + attachment.mMimeType + ";"); 213 mockTransport.expect(" name=\"" + attachment.mFileName + "\""); 214 mockTransport.expect("Content-Transfer-Encoding: base64"); 215 mockTransport.expect("Content-Disposition: attachment;"); 216 mockTransport.expect(" filename=\"" + attachment.mFileName + "\";"); 217 mockTransport.expect(" size=" + Long.toString(attachment.mSize)); 218 mockTransport.expect(""); 219 String attachmentContentUri = attachment.getContentUri(); 220 if (attachmentContentUri != null && attachmentContentUri.startsWith("file://")) { 221 mockTransport.expect(TEST_STRING_BASE64); 222 } 223 } 224 225 /** 226 * Test: Recover from a server closing early (or returning an empty string) 227 */ 228 public void testEmptyLineResponse() throws Exception { 229 MockTransport mockTransport = openAndInjectMockTransport(); 230 231 // Since SmtpSender.sendMessage() does a close then open, we need to preset for the open 232 mockTransport.expectClose(); 233 234 // Load up just the bare minimum to expose the error 235 mockTransport.expect(null, "220 MockTransport 2000 Ready To Assist You Peewee"); 236 mockTransport.expectLiterally("EHLO [" + LOCAL_ADDRESS + "]", null); 237 238 // Now trigger the transmission 239 // Note, a null message is sufficient here, as we won't even get past open() 240 try { 241 mSender.sendMessage(-1); 242 fail("Should not be able to send with failed open()"); 243 } catch (MessagingException me) { 244 // good - expected 245 // TODO maybe expect a particular exception? 246 } 247 } 248 249 /** 250 * Set up a basic MockTransport. open it, and inject it into mStore 251 */ 252 private MockTransport openAndInjectMockTransport() throws UnknownHostException { 253 // Create mock transport and inject it into the SmtpSender that's already set up 254 MockTransport mockTransport = new MockTransport(mContext, mHostAuth); 255 mockTransport.setSecurity(HostAuth.FLAG_NONE, false); 256 mSender.setTransport(mockTransport); 257 mockTransport.setMockLocalAddress(InetAddress.getByName(LOCAL_ADDRESS)); 258 return mockTransport; 259 } 260 261 /** 262 * Helper which stuffs the mock with enough strings to satisfy a call to SmtpSender.open() 263 * 264 * @param mockTransport the mock transport we're using 265 * @param capabilities if non-null, comma-separated list of capabilities 266 */ 267 private void setupOpen(MockTransport mockTransport, String capabilities) { 268 mockTransport.expect(null, "220 MockTransport 2000 Ready To Assist You Peewee"); 269 mockTransport.expect("EHLO .*", "250-10.20.30.40 hello"); 270 if (capabilities == null) { 271 mockTransport.expect(null, "250-HELP"); 272 mockTransport.expect(null, "250-AUTH LOGIN PLAIN CRAM-MD5"); 273 mockTransport.expect(null, "250-SIZE 15728640"); 274 mockTransport.expect(null, "250-ENHANCEDSTATUSCODES"); 275 mockTransport.expect(null, "250-8BITMIME"); 276 } else { 277 for (String capability : capabilities.split(",")) { 278 mockTransport.expect(null, "250-" + capability); 279 } 280 } 281 mockTransport.expect(null, "250+OK"); 282 mockTransport.expect("AUTH PLAIN .*", "235 2.7.0 ... authentication succeeded"); 283 } 284 } 285