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