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