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