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