MailTransport.java revision 313586c8eb4e23ceec068b82f3dc0be1c8a7045f
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.Email;
20import com.android.email.mail.Transport;
21import com.android.emailcommon.Logging;
22import com.android.emailcommon.mail.CertificateValidationException;
23import com.android.emailcommon.mail.MessagingException;
24import com.android.emailcommon.utility.SSLUtils;
25
26import android.util.Log;
27
28import java.io.BufferedInputStream;
29import java.io.BufferedOutputStream;
30import java.io.IOException;
31import java.io.InputStream;
32import java.io.OutputStream;
33import java.net.InetAddress;
34import java.net.InetSocketAddress;
35import java.net.Socket;
36import java.net.SocketAddress;
37import java.net.SocketException;
38
39import javax.net.ssl.HostnameVerifier;
40import javax.net.ssl.HttpsURLConnection;
41import javax.net.ssl.SSLException;
42import javax.net.ssl.SSLPeerUnverifiedException;
43import javax.net.ssl.SSLSession;
44import javax.net.ssl.SSLSocket;
45
46/**
47 * This class implements the common aspects of "transport", one layer below the
48 * specific wire protocols such as POP3, IMAP, or SMTP.
49 */
50public class MailTransport implements Transport {
51
52    // TODO protected eventually
53    /*protected*/ public static final int SOCKET_CONNECT_TIMEOUT = 10000;
54    /*protected*/ public static final int SOCKET_READ_TIMEOUT = 60000;
55
56    private static final HostnameVerifier HOSTNAME_VERIFIER =
57            HttpsURLConnection.getDefaultHostnameVerifier();
58
59    private String mDebugLabel;
60
61    private String mHost;
62    private int mPort;
63    private String[] mUserInfoParts;
64
65    /**
66     * One of the {@code Transport.CONNECTION_SECURITY_*} values.
67     */
68    private int mConnectionSecurity;
69
70    /**
71     * Whether or not to trust all server certificates (i.e. skip host verification) in SSL
72     * handshakes
73     */
74    private boolean mTrustCertificates;
75
76    private Socket mSocket;
77    private InputStream mIn;
78    private OutputStream mOut;
79
80    /**
81     * Simple constructor for starting from scratch.  Call setUri() and setSecurity() to
82     * complete the configuration.
83     * @param debugLabel Label used for Log.d calls
84     */
85    public MailTransport(String debugLabel) {
86        super();
87        mDebugLabel = debugLabel;
88    }
89
90    /**
91     * Returns a new transport, using the current transport as a model. The new transport is
92     * configured identically (as if {@link #setSecurity(int, boolean)}, {@link #setPort(int)}
93     * and {@link #setHost(String)} were invoked), but not opened or connected in any way.
94     */
95    @Override
96    public Transport clone() {
97        MailTransport newObject = new MailTransport(mDebugLabel);
98
99        newObject.mDebugLabel = mDebugLabel;
100        newObject.mHost = mHost;
101        newObject.mPort = mPort;
102        if (mUserInfoParts != null) {
103            newObject.mUserInfoParts = mUserInfoParts.clone();
104        }
105        newObject.mConnectionSecurity = mConnectionSecurity;
106        newObject.mTrustCertificates = mTrustCertificates;
107        return newObject;
108    }
109
110    @Override
111    public void setHost(String host) {
112        mHost = host;
113    }
114
115    @Override
116    public void setPort(int port) {
117        mPort = port;
118    }
119
120    @Override
121    public String getHost() {
122        return mHost;
123    }
124
125    @Override
126    public int getPort() {
127        return mPort;
128    }
129
130    @Override
131    public void setSecurity(int connectionSecurity, boolean trustAllCertificates) {
132        mConnectionSecurity = connectionSecurity;
133        mTrustCertificates = trustAllCertificates;
134    }
135
136    @Override
137    public int getSecurity() {
138        return mConnectionSecurity;
139    }
140
141    @Override
142    public boolean canTrySslSecurity() {
143        return mConnectionSecurity == Transport.CONNECTION_SECURITY_SSL;
144    }
145
146    @Override
147    public boolean canTryTlsSecurity() {
148        return mConnectionSecurity == Transport.CONNECTION_SECURITY_TLS;
149    }
150
151    @Override
152    public boolean canTrustAllCertificates() {
153        return mTrustCertificates;
154    }
155
156    /**
157     * Attempts to open a connection using the Uri supplied for connection parameters.  Will attempt
158     * an SSL connection if indicated.
159     */
160    @Override
161    public void open() throws MessagingException, CertificateValidationException {
162        if (Email.DEBUG) {
163            Log.d(Logging.LOG_TAG, "*** " + mDebugLabel + " open " +
164                    getHost() + ":" + String.valueOf(getPort()));
165        }
166
167        try {
168            SocketAddress socketAddress = new InetSocketAddress(getHost(), getPort());
169            if (canTrySslSecurity()) {
170                mSocket = SSLUtils.getSSLSocketFactory(canTrustAllCertificates()).createSocket();
171            } else {
172                mSocket = new Socket();
173            }
174            mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT);
175            // After the socket connects to an SSL server, confirm that the hostname is as expected
176            if (canTrySslSecurity() && !canTrustAllCertificates()) {
177                verifyHostname(mSocket, getHost());
178            }
179            mIn = new BufferedInputStream(mSocket.getInputStream(), 1024);
180            mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
181
182        } catch (SSLException e) {
183            if (Email.DEBUG) {
184                Log.d(Logging.LOG_TAG, e.toString());
185            }
186            throw new CertificateValidationException(e.getMessage(), e);
187        } catch (IOException ioe) {
188            if (Email.DEBUG) {
189                Log.d(Logging.LOG_TAG, ioe.toString());
190            }
191            throw new MessagingException(MessagingException.IOERROR, ioe.toString());
192        }
193    }
194
195    /**
196     * Attempts to reopen a TLS connection using the Uri supplied for connection parameters.
197     *
198     * NOTE: No explicit hostname verification is required here, because it's handled automatically
199     * by the call to createSocket().
200     *
201     * TODO should we explicitly close the old socket?  This seems funky to abandon it.
202     */
203    @Override
204    public void reopenTls() throws MessagingException {
205        try {
206            mSocket = SSLUtils.getSSLSocketFactory(canTrustAllCertificates())
207                    .createSocket(mSocket, getHost(), getPort(), true);
208            mSocket.setSoTimeout(SOCKET_READ_TIMEOUT);
209            mIn = new BufferedInputStream(mSocket.getInputStream(), 1024);
210            mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
211
212        } catch (SSLException e) {
213            if (Email.DEBUG) {
214                Log.d(Logging.LOG_TAG, e.toString());
215            }
216            throw new CertificateValidationException(e.getMessage(), e);
217        } catch (IOException ioe) {
218            if (Email.DEBUG) {
219                Log.d(Logging.LOG_TAG, ioe.toString());
220            }
221            throw new MessagingException(MessagingException.IOERROR, ioe.toString());
222        }
223    }
224
225    /**
226     * Lightweight version of SSLCertificateSocketFactory.verifyHostname, which provides this
227     * service but is not in the public API.
228     *
229     * Verify the hostname of the certificate used by the other end of a
230     * connected socket.  You MUST call this if you did not supply a hostname
231     * to SSLCertificateSocketFactory.createSocket().  It is harmless to call this method
232     * redundantly if the hostname has already been verified.
233     *
234     * <p>Wildcard certificates are allowed to verify any matching hostname,
235     * so "foo.bar.example.com" is verified if the peer has a certificate
236     * for "*.example.com".
237     *
238     * @param socket An SSL socket which has been connected to a server
239     * @param hostname The expected hostname of the remote server
240     * @throws IOException if something goes wrong handshaking with the server
241     * @throws SSLPeerUnverifiedException if the server cannot prove its identity
242      */
243    private void verifyHostname(Socket socket, String hostname) throws IOException {
244        // The code at the start of OpenSSLSocketImpl.startHandshake()
245        // ensures that the call is idempotent, so we can safely call it.
246        SSLSocket ssl = (SSLSocket) socket;
247        ssl.startHandshake();
248
249        SSLSession session = ssl.getSession();
250        if (session == null) {
251            throw new SSLException("Cannot verify SSL socket without session");
252        }
253        // TODO: Instead of reporting the name of the server we think we're connecting to,
254        // we should be reporting the bad name in the certificate.  Unfortunately this is buried
255        // in the verifier code and is not available in the verifier API, and extracting the
256        // CN & alts is beyond the scope of this patch.
257        if (!HOSTNAME_VERIFIER.verify(hostname, session)) {
258            throw new SSLPeerUnverifiedException(
259                    "Certificate hostname not useable for server: " + hostname);
260        }
261    }
262
263    /**
264     * Set the socket timeout.
265     * @param timeoutMilliseconds the read timeout value if greater than {@code 0}, or
266     *            {@code 0} for an infinite timeout.
267     */
268    @Override
269    public void setSoTimeout(int timeoutMilliseconds) throws SocketException {
270        mSocket.setSoTimeout(timeoutMilliseconds);
271    }
272
273    @Override
274    public boolean isOpen() {
275        return (mIn != null && mOut != null &&
276                mSocket != null && mSocket.isConnected() && !mSocket.isClosed());
277    }
278
279    /**
280     * Close the connection.  MUST NOT return any exceptions - must be "best effort" and safe.
281     */
282    @Override
283    public void close() {
284        try {
285            mIn.close();
286        } catch (Exception e) {
287            // May fail if the connection is already closed.
288        }
289        try {
290            mOut.close();
291        } catch (Exception e) {
292            // May fail if the connection is already closed.
293        }
294        try {
295            mSocket.close();
296        } catch (Exception e) {
297            // May fail if the connection is already closed.
298        }
299        mIn = null;
300        mOut = null;
301        mSocket = null;
302    }
303
304    @Override
305    public InputStream getInputStream() {
306        return mIn;
307    }
308
309    @Override
310    public OutputStream getOutputStream() {
311        return mOut;
312    }
313
314    /**
315     * Writes a single line to the server using \r\n termination.
316     */
317    @Override
318    public void writeLine(String s, String sensitiveReplacement) throws IOException {
319        if (Email.DEBUG) {
320            if (sensitiveReplacement != null && !Logging.DEBUG_SENSITIVE) {
321                Log.d(Logging.LOG_TAG, ">>> " + sensitiveReplacement);
322            } else {
323                Log.d(Logging.LOG_TAG, ">>> " + s);
324            }
325        }
326
327        OutputStream out = getOutputStream();
328        out.write(s.getBytes());
329        out.write('\r');
330        out.write('\n');
331        out.flush();
332    }
333
334    /**
335     * Reads a single line from the server, using either \r\n or \n as the delimiter.  The
336     * delimiter char(s) are not included in the result.
337     */
338    @Override
339    public String readLine() throws IOException {
340        StringBuffer sb = new StringBuffer();
341        InputStream in = getInputStream();
342        int d;
343        while ((d = in.read()) != -1) {
344            if (((char)d) == '\r') {
345                continue;
346            } else if (((char)d) == '\n') {
347                break;
348            } else {
349                sb.append((char)d);
350            }
351        }
352        if (d == -1 && Email.DEBUG) {
353            Log.d(Logging.LOG_TAG, "End of stream reached while trying to read line.");
354        }
355        String ret = sb.toString();
356        if (Email.DEBUG) {
357            Log.d(Logging.LOG_TAG, "<<< " + ret);
358        }
359        return ret;
360    }
361
362    @Override
363    public InetAddress getLocalAddress() {
364        if (isOpen()) {
365            return mSocket.getLocalAddress();
366        } else {
367            return null;
368        }
369    }
370}
371