1/*
2 * Copyright (C) 2015 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 */
16package com.android.phone.common.mail;
17
18import android.content.Context;
19import android.net.Network;
20
21import com.android.phone.common.mail.store.ImapStore;
22import com.android.phone.common.mail.utils.LogUtils;
23
24import java.net.SocketAddress;
25import java.util.ArrayList;
26import java.util.List;
27
28import javax.net.ssl.HostnameVerifier;
29import javax.net.ssl.HttpsURLConnection;
30import javax.net.ssl.SSLException;
31import javax.net.ssl.SSLPeerUnverifiedException;
32import javax.net.ssl.SSLSession;
33import javax.net.ssl.SSLSocket;
34
35import java.io.BufferedInputStream;
36import java.io.BufferedOutputStream;
37import java.io.IOException;
38import java.io.InputStream;
39import java.io.OutputStream;
40import java.net.InetAddress;
41import java.net.InetSocketAddress;
42import java.net.Socket;
43
44/**
45 * Make connection and perform operations on mail server by reading and writing lines.
46 */
47public class MailTransport {
48    private static final String TAG = "MailTransport";
49
50    // TODO protected eventually
51    /*protected*/ public static final int SOCKET_CONNECT_TIMEOUT = 10000;
52    /*protected*/ public static final int SOCKET_READ_TIMEOUT = 60000;
53
54    private static final HostnameVerifier HOSTNAME_VERIFIER =
55            HttpsURLConnection.getDefaultHostnameVerifier();
56
57    private Context mContext;
58    private Network mNetwork;
59    private String mHost;
60    private int mPort;
61    private Socket mSocket;
62    private BufferedInputStream mIn;
63    private BufferedOutputStream mOut;
64    private int mFlags;
65
66    public MailTransport(Context context, Network network, String address, int port, int flags) {
67        mContext = context;
68        mNetwork = network;
69        mHost = address;
70        mPort = port;
71        mFlags = flags;
72    }
73
74    /**
75     * Returns a new transport, using the current transport as a model. The new transport is
76     * configured identically, but not opened or connected in any way.
77     */
78    @Override
79    public MailTransport clone() {
80        return new MailTransport(mContext, mNetwork, mHost, mPort, mFlags);
81    }
82
83    public boolean canTrySslSecurity() {
84        return (mFlags & ImapStore.FLAG_SSL) != 0;
85    }
86
87    public boolean canTrustAllCertificates() {
88        return (mFlags & ImapStore.FLAG_TRUST_ALL) != 0;
89    }
90
91    /**
92     * Attempts to open a connection using the Uri supplied for connection parameters.  Will attempt
93     * an SSL connection if indicated.
94     */
95    public void open() throws MessagingException, CertificateValidationException {
96        LogUtils.d(TAG, "*** IMAP open " + mHost + ":" + String.valueOf(mPort));
97
98        List<SocketAddress> socketAddresses = new ArrayList<SocketAddress>();
99        try {
100            if (canTrySslSecurity()) {
101                mSocket = HttpsURLConnection.getDefaultSSLSocketFactory().createSocket();
102                socketAddresses.add(new InetSocketAddress(mHost, mPort));
103            } else {
104                if (mNetwork == null) {
105                    mSocket = new Socket();
106                    socketAddresses.add(new InetSocketAddress(mHost, mPort));
107                } else {
108                    InetAddress[] inetAddresses = mNetwork.getAllByName(mHost);
109                    for (int i = 0; i < inetAddresses.length; i++) {
110                        socketAddresses.add(new InetSocketAddress(inetAddresses[i], mPort));
111                    }
112                    mSocket = mNetwork.getSocketFactory().createSocket();
113                }
114            }
115        } catch (IOException ioe) {
116            LogUtils.d(TAG, ioe.toString());
117            throw new MessagingException(MessagingException.IOERROR, ioe.toString());
118        }
119
120        while (socketAddresses.size() > 0) {
121            try {
122                mSocket.connect(socketAddresses.remove(0), SOCKET_CONNECT_TIMEOUT);
123
124                // After the socket connects to an SSL server, confirm that the hostname is as
125                // expected
126                if (canTrySslSecurity() && !canTrustAllCertificates()) {
127                    verifyHostname(mSocket, mHost);
128                }
129
130                mIn = new BufferedInputStream(mSocket.getInputStream(), 1024);
131                mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
132                mSocket.setSoTimeout(SOCKET_READ_TIMEOUT);
133                return;
134            } catch (IOException ioe) {
135                LogUtils.d(TAG, ioe.toString());
136                if (socketAddresses.size() == 0) {
137                    // Only throw an error when there are no more sockets to try.
138                    throw new MessagingException(MessagingException.IOERROR, ioe.toString());
139                }
140            }
141        }
142    }
143
144    /**
145     * Lightweight version of SSLCertificateSocketFactory.verifyHostname, which provides this
146     * service but is not in the public API.
147     *
148     * Verify the hostname of the certificate used by the other end of a
149     * connected socket. It is harmless to call this method redundantly if the hostname has already
150     * been verified.
151     *
152     * <p>Wildcard certificates are allowed to verify any matching hostname,
153     * so "foo.bar.example.com" is verified if the peer has a certificate
154     * for "*.example.com".
155     *
156     * @param socket An SSL socket which has been connected to a server
157     * @param hostname The expected hostname of the remote server
158     * @throws IOException if something goes wrong handshaking with the server
159     * @throws SSLPeerUnverifiedException if the server cannot prove its identity
160      */
161    private static void verifyHostname(Socket socket, String hostname) throws IOException {
162        // The code at the start of OpenSSLSocketImpl.startHandshake()
163        // ensures that the call is idempotent, so we can safely call it.
164        SSLSocket ssl = (SSLSocket) socket;
165        ssl.startHandshake();
166
167        SSLSession session = ssl.getSession();
168        if (session == null) {
169            throw new SSLException("Cannot verify SSL socket without session");
170        }
171        // TODO: Instead of reporting the name of the server we think we're connecting to,
172        // we should be reporting the bad name in the certificate.  Unfortunately this is buried
173        // in the verifier code and is not available in the verifier API, and extracting the
174        // CN & alts is beyond the scope of this patch.
175        if (!HOSTNAME_VERIFIER.verify(hostname, session)) {
176            throw new SSLPeerUnverifiedException(
177                    "Certificate hostname not useable for server: " + hostname);
178        }
179    }
180
181    public boolean isOpen() {
182        return (mIn != null && mOut != null &&
183                mSocket != null && mSocket.isConnected() && !mSocket.isClosed());
184    }
185
186    /**
187     * Close the connection.  MUST NOT return any exceptions - must be "best effort" and safe.
188     */
189    public void close() {
190        try {
191            mIn.close();
192        } catch (Exception e) {
193            // May fail if the connection is already closed.
194        }
195        try {
196            mOut.close();
197        } catch (Exception e) {
198            // May fail if the connection is already closed.
199        }
200        try {
201            mSocket.close();
202        } catch (Exception e) {
203            // May fail if the connection is already closed.
204        }
205        mIn = null;
206        mOut = null;
207        mSocket = null;
208    }
209
210    public InputStream getInputStream() {
211        return mIn;
212    }
213
214    public OutputStream getOutputStream() {
215        return mOut;
216    }
217
218    /**
219     * Writes a single line to the server using \r\n termination.
220     */
221    public void writeLine(String s, String sensitiveReplacement) throws IOException {
222        if (sensitiveReplacement != null) {
223            LogUtils.d(TAG, ">>> " + sensitiveReplacement);
224        } else {
225            LogUtils.d(TAG, ">>> " + s);
226        }
227
228        OutputStream out = getOutputStream();
229        out.write(s.getBytes());
230        out.write('\r');
231        out.write('\n');
232        out.flush();
233    }
234
235    /**
236     * Reads a single line from the server, using either \r\n or \n as the delimiter.  The
237     * delimiter char(s) are not included in the result.
238     */
239    public String readLine(boolean loggable) throws IOException {
240        StringBuffer sb = new StringBuffer();
241        InputStream in = getInputStream();
242        int d;
243        while ((d = in.read()) != -1) {
244            if (((char)d) == '\r') {
245                continue;
246            } else if (((char)d) == '\n') {
247                break;
248            } else {
249                sb.append((char)d);
250            }
251        }
252        if (d == -1) {
253            LogUtils.d(TAG, "End of stream reached while trying to read line.");
254        }
255        String ret = sb.toString();
256        if (loggable) {
257            LogUtils.d(TAG, "<<< " + ret);
258        }
259        return ret;
260    }
261}
262