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