1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5  * use this file except in compliance with the License. You may obtain a copy of
6  * 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, WITHOUT
12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13  * License for the specific language governing permissions and limitations under
14  * the License.
15  */
16 
17 package com.android.email.mail.transport;
18 
19 import android.content.Context;
20 
21 import com.android.emailcommon.provider.HostAuth;
22 import com.android.mail.utils.LogUtils;
23 
24 import junit.framework.Assert;
25 
26 import java.io.IOException;
27 import java.io.InputStream;
28 import java.io.OutputStream;
29 import java.net.InetAddress;
30 import java.util.ArrayList;
31 import java.util.Arrays;
32 import java.util.regex.Pattern;
33 
34 /**
35  * This is a mock Transport that is used to test protocols that use MailTransport.
36  */
37 public class MockTransport extends MailTransport {
38 
39     // All flags defining debug or development code settings must be FALSE
40     // when code is checked in or released.
41     private static boolean DEBUG_LOG_STREAMS = true;
42 
43     private static String LOG_TAG = "MockTransport";
44 
45     private static final String SPECIAL_RESPONSE_IOEXCEPTION = "!!!IOEXCEPTION!!!";
46 
47     private boolean mTlsStarted = false;
48 
49     private boolean mOpen;
50     private boolean mInputOpen;
51     private InetAddress mLocalAddress;
52 
53     private ArrayList<String> mQueuedInput = new ArrayList<String>();
54 
55     private static class Transaction {
56         public static final int ACTION_INJECT_TEXT = 0;
57         public static final int ACTION_CLIENT_CLOSE = 1;
58         public static final int ACTION_IO_EXCEPTION = 2;
59         public static final int ACTION_START_TLS = 3;
60 
61         int mAction;
62         String mPattern;
63         String[] mResponses;
64 
Transaction(String pattern, String[] responses)65         Transaction(String pattern, String[] responses) {
66             mAction = ACTION_INJECT_TEXT;
67             mPattern = pattern;
68             mResponses = responses;
69         }
70 
Transaction(int otherType)71         Transaction(int otherType) {
72             mAction = otherType;
73             mPattern = null;
74             mResponses = null;
75         }
76 
77         @Override
toString()78         public String toString() {
79             switch (mAction) {
80                 case ACTION_INJECT_TEXT:
81                     return mPattern + ": " + Arrays.toString(mResponses);
82                 case ACTION_CLIENT_CLOSE:
83                     return "Expect the client to close";
84                 case ACTION_IO_EXCEPTION:
85                     return "Expect IOException";
86                 case ACTION_START_TLS:
87                     return "Expect StartTls";
88                 default:
89                     return "(Hmm.  Unknown action.)";
90             }
91         }
92     }
93 
94     private ArrayList<Transaction> mPairs = new ArrayList<Transaction>();
95 
createMockTransport(Context context)96     public static MockTransport createMockTransport(Context context) {
97         return new MockTransport(context, new HostAuth());
98     }
99 
MockTransport(Context context, HostAuth hostAuth)100     public MockTransport(Context context, HostAuth hostAuth) {
101         super(context, LOG_TAG, hostAuth);
102     }
103 
104     /**
105      * Give the mock a pattern to wait for.  No response will be sent.
106      * @param pattern Java RegEx to wait for
107      */
expect(String pattern)108     public void expect(String pattern) {
109         expect(pattern, (String[]) null);
110     }
111 
112     /**
113      * Give the mock a pattern to wait for and a response to send back.
114      * @param pattern Java RegEx to wait for
115      * @param response String to reply with, or null to acccept string but not respond to it
116      */
expect(String pattern, String response)117     public void expect(String pattern, String response) {
118         expect(pattern, (response == null) ? null : new String[] {response});
119     }
120 
121     /**
122      * Give the mock a pattern to wait for and a multi-line response to send back.
123      * @param pattern Java RegEx to wait for
124      * @param responses Strings to reply with
125      */
expect(String pattern, String[] responses)126     public void expect(String pattern, String[] responses) {
127         Transaction pair = new Transaction(pattern, responses);
128         mPairs.add(pair);
129     }
130 
131     /**
132      * Same as {@link #expect(String, String[])}, but the first arg is taken literally, rather than
133      * as a regexp.
134      */
expectLiterally(String literal, String[] responses)135     public void expectLiterally(String literal, String[] responses) {
136         expect("^" + Pattern.quote(literal) + "$", responses);
137     }
138 
139     /**
140      * Tell the Mock Transport that we expect it to be closed.  This will preserve
141      * the remaining entries in the expect() stream and allow us to "ride over" the close (which
142      * would normally reset everything).
143      */
expectClose()144     public void expectClose() {
145         mPairs.add(new Transaction(Transaction.ACTION_CLIENT_CLOSE));
146     }
147 
expectIOException()148     public void expectIOException() {
149         mPairs.add(new Transaction(Transaction.ACTION_IO_EXCEPTION));
150     }
151 
expectStartTls()152     public void expectStartTls() {
153         mPairs.add(new Transaction(Transaction.ACTION_START_TLS));
154     }
155 
sendResponse(Transaction pair)156     private void sendResponse(Transaction pair) {
157         switch (pair.mAction) {
158             case Transaction.ACTION_INJECT_TEXT:
159                 for (String s : pair.mResponses) {
160                     mQueuedInput.add(s);
161                 }
162                 break;
163             case Transaction.ACTION_IO_EXCEPTION:
164                 mQueuedInput.add(SPECIAL_RESPONSE_IOEXCEPTION);
165                 break;
166             default:
167                 Assert.fail("Invalid action for sendResponse: " + pair.mAction);
168         }
169     }
170 
171     /**
172      * Check that TLS was started
173      */
isTlsStarted()174     public boolean isTlsStarted() {
175         return mTlsStarted;
176     }
177 
178     /**
179      * This simulates a condition where the server has closed its side, causing
180      * reads to fail.
181      */
closeInputStream()182     public void closeInputStream() {
183         mInputOpen = false;
184     }
185 
186     @Override
close()187     public void close() {
188         mOpen = false;
189         mInputOpen = false;
190         // unless it was expected as part of a test, reset the stream
191         if (mPairs.size() > 0) {
192             Transaction expect = mPairs.remove(0);
193             if (expect.mAction == Transaction.ACTION_CLIENT_CLOSE) {
194                 return;
195             }
196         }
197         mQueuedInput.clear();
198         mPairs.clear();
199     }
200 
setMockLocalAddress(InetAddress address)201     public void setMockLocalAddress(InetAddress address) {
202         mLocalAddress = address;
203     }
204 
205     @Override
getInputStream()206     public InputStream getInputStream() {
207         SmtpSenderUnitTests.assertTrue(mOpen);
208         return new MockInputStream();
209     }
210 
211     /**
212      * This normally serves as a pseudo-clone, for use by Imap.  For the purposes of unit testing,
213      * until we need something more complex, we'll just return the actual MockTransport.  Then we
214      * don't have to worry about dealing with test metadata like the expects list or socket state.
215      */
216     @Override
clone()217     public MockTransport clone() {
218         return this;
219     }
220 
221     @Override
getOutputStream()222     public OutputStream getOutputStream() {
223         Assert.assertTrue(mOpen);
224         return new MockOutputStream();
225     }
226 
227 
228     @Override
isOpen()229     public boolean isOpen() {
230         return mOpen;
231     }
232 
233     @Override
open()234     public void open() /*
235                         * throws MessagingException,
236                         * CertificateValidationException
237                         */{
238         mOpen = true;
239         mInputOpen = true;
240     }
241 
242     /**
243      * This returns one string (if available) to the caller.  Usually this simply pulls strings
244      * from the mQueuedInput list, but if the list is empty, we also peek the expect list.  This
245      * supports banners, multi-line responses, and any other cases where we respond without
246      * a specific expect pattern.
247      *
248      * If no response text is available, we assert (failing our test) as an underflow.
249      *
250      * Logs the read text if DEBUG_LOG_STREAMS is true.
251      */
readLine()252     public String readLine() throws IOException {
253         SmtpSenderUnitTests.assertTrue(mOpen);
254         if (!mInputOpen) {
255             throw new IOException("Reading from MockTransport with closed input");
256         }
257         // if there's nothing to read, see if we can find a null-pattern
258         // response
259         if ((mQueuedInput.size() == 0) && (mPairs.size() > 0)) {
260             Transaction pair = mPairs.get(0);
261             if (pair.mPattern == null) {
262                 mPairs.remove(0);
263                 sendResponse(pair);
264             }
265         }
266         if (mQueuedInput.size() == 0) {
267             // MailTransport returns "" at EOS.
268             LogUtils.w(LOG_TAG, "Underflow reading from MockTransport");
269             return "";
270         }
271         String line = mQueuedInput.remove(0);
272         if (DEBUG_LOG_STREAMS) {
273             LogUtils.d(LOG_TAG, "<<< " + line);
274         }
275         if (SPECIAL_RESPONSE_IOEXCEPTION.equals(line)) {
276             throw new IOException("Expected IOException.");
277         }
278         return line;
279     }
280 
281     @Override
reopenTls()282     public void reopenTls() /* throws MessagingException */{
283         SmtpSenderUnitTests.assertTrue(mOpen);
284         Transaction expect = mPairs.remove(0);
285         SmtpSenderUnitTests.assertTrue(expect.mAction == Transaction.ACTION_START_TLS);
286         mTlsStarted = true;
287     }
288 
setSecurity(int connectionSecurity, boolean trustAllCertificates)289     public void setSecurity(int connectionSecurity, boolean trustAllCertificates) {
290         mHostAuth.mFlags =
291                 connectionSecurity & (trustAllCertificates ? HostAuth.FLAG_TRUST_ALL : 0xff);
292     }
293 
setHost(String address)294     public void setHost(String address) {
295         mHostAuth.mAddress = address;
296     }
297 
298     @Override
setSoTimeout(int timeoutMilliseconds)299     public void setSoTimeout(int timeoutMilliseconds) /* throws SocketException */{}
300 
301     /**
302      * Accepts a single string (command or text) that was written by the code under test.
303      * Because we are essentially mocking a server, we check to see if this string was expected.
304      * If the string was expected, we push the corresponding responses into the mQueuedInput
305      * list, for subsequent calls to readLine().  If the string does not match, we assert
306      * the mismatch.  If no string was expected, we assert it as an overflow.
307      *
308      * Logs the written text if DEBUG_LOG_STREAMS is true.
309      */
310     @Override
writeLine(String s, String sensitiveReplacement)311     public void writeLine(String s, String sensitiveReplacement) throws IOException {
312         if (DEBUG_LOG_STREAMS) {
313             LogUtils.d(LOG_TAG, ">>> " + s);
314         }
315         SmtpSenderUnitTests.assertTrue(mOpen);
316         SmtpSenderUnitTests.assertTrue(
317                 "Overflow writing to MockTransport: Getting " + s, 0 != mPairs.size());
318         Transaction pair = mPairs.remove(0);
319         if (pair.mAction == Transaction.ACTION_IO_EXCEPTION) {
320             throw new IOException("Expected IOException.");
321         }
322         SmtpSenderUnitTests.assertTrue("Unexpected string written to MockTransport: Actual=" + s
323                 + "  Expected=" + pair.mPattern, pair.mPattern != null && s.matches(pair.mPattern));
324         if (pair.mResponses != null) {
325             sendResponse(pair);
326         }
327     }
328 
329     /**
330      * This is an InputStream that satisfies the needs of getInputStream()
331      */
332     private class MockInputStream extends InputStream {
333 
334         private byte[] mNextLine = null;
335         private int mNextIndex = 0;
336 
337         /**
338          * Reads from the same input buffer as readLine()
339          */
340         @Override
read()341         public int read() throws IOException {
342             if (!mInputOpen) {
343                 throw new IOException();
344             }
345 
346             if (mNextLine != null && mNextIndex < mNextLine.length) {
347                 return mNextLine[mNextIndex++];
348             }
349 
350             // previous line was exhausted so try to get another one
351             String next = readLine();
352             if (next == null) {
353                 throw new IOException("Reading from MockTransport with closed input");
354             }
355             mNextLine = (next + "\r\n").getBytes();
356             mNextIndex = 0;
357 
358             if (mNextLine != null && mNextIndex < mNextLine.length) {
359                 return mNextLine[mNextIndex++];
360             }
361 
362             // no joy - throw an exception
363             throw new IOException();
364         }
365     }
366 
367     /**
368      * This is an OutputStream that satisfies the needs of getOutputStream()
369      */
370     private class MockOutputStream extends OutputStream {
371 
372         private StringBuilder sb = new StringBuilder();
373 
374         @Override
write(int oneByte)375         public void write(int oneByte) throws IOException {
376             // CR or CRLF will immediately dump previous line (w/o CRLF)
377             if (oneByte == '\r') {
378                 writeLine(sb.toString(), null);
379                 sb = new StringBuilder();
380             } else if (oneByte == '\n') {
381                 // swallow it
382             } else {
383                 sb.append((char) oneByte);
384             }
385         }
386     }
387 
388     @Override
getLocalAddress()389     public InetAddress getLocalAddress() {
390         if (isOpen()) {
391             return mLocalAddress;
392         } else {
393             return null;
394         }
395     }
396 }
397