MailTransport.java revision fb060de65db57607748cbf8bc5b93939281a443f
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.CertificateValidationException;
21import com.android.email.mail.MessagingException;
22import com.android.email.mail.Transport;
23
24import android.util.Config;
25import android.util.Log;
26
27import java.io.BufferedInputStream;
28import java.io.BufferedOutputStream;
29import java.io.IOException;
30import java.io.InputStream;
31import java.io.OutputStream;
32import java.net.InetSocketAddress;
33import java.net.Socket;
34import java.net.SocketAddress;
35import java.net.SocketException;
36import java.net.URI;
37
38import javax.net.ssl.HostnameVerifier;
39import javax.net.ssl.HttpsURLConnection;
40import javax.net.ssl.SSLException;
41import javax.net.ssl.SSLPeerUnverifiedException;
42import javax.net.ssl.SSLSession;
43import javax.net.ssl.SSLSocket;
44
45/**
46 * This class implements the common aspects of "transport", one layer below the
47 * specific wire protocols such as POP3, IMAP, or SMTP.
48 */
49public class MailTransport implements Transport {
50
51    // TODO protected eventually
52    /*protected*/ public static final int SOCKET_CONNECT_TIMEOUT = 10000;
53    /*protected*/ public static final int SOCKET_READ_TIMEOUT = 60000;
54
55    private static final HostnameVerifier HOSTNAME_VERIFIER =
56            HttpsURLConnection.getDefaultHostnameVerifier();
57
58    private String mDebugLabel;
59
60    private String mHost;
61    private int mPort;
62    private String[] mUserInfoParts;
63    private int mConnectionSecurity;
64    private boolean mTrustCertificates;
65
66    private Socket mSocket;
67    private InputStream mIn;
68    private OutputStream mOut;
69
70    /**
71     * Simple constructor for starting from scratch.  Call setUri() and setSecurity() to
72     * complete the configuration.
73     * @param debugLabel Label used for Log.d calls
74     */
75    public MailTransport(String debugLabel) {
76        super();
77        mDebugLabel = debugLabel;
78    }
79
80    /**
81     * Get a new transport, using an existing one as a model.  The new transport is configured as if
82     * setUri() and setSecurity() have been called, but not opened or connected in any way.
83     * @return a new Transport ready to open()
84     */
85    public Transport newInstanceWithConfiguration() {
86        MailTransport newObject = new MailTransport(mDebugLabel);
87
88        newObject.mDebugLabel = mDebugLabel;
89        newObject.mHost = mHost;
90        newObject.mPort = mPort;
91        if (mUserInfoParts != null) {
92            newObject.mUserInfoParts = mUserInfoParts.clone();
93        }
94        newObject.mConnectionSecurity = mConnectionSecurity;
95        newObject.mTrustCertificates = mTrustCertificates;
96        return newObject;
97    }
98
99    public void setUri(URI uri, int defaultPort) {
100        mHost = uri.getHost();
101
102        mPort = defaultPort;
103        if (uri.getPort() != -1) {
104            mPort = uri.getPort();
105        }
106
107        if (uri.getUserInfo() != null) {
108            mUserInfoParts = uri.getUserInfo().split(":", 2);
109        }
110
111    }
112
113    public String[] getUserInfoParts() {
114        return mUserInfoParts;
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 (Config.LOGD && Email.DEBUG) {
152            Log.d(Email.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 (Config.LOGD && Email.DEBUG) {
173                Log.d(Email.LOG_TAG, e.toString());
174            }
175            throw new CertificateValidationException(e.getMessage(), e);
176        } catch (IOException ioe) {
177            if (Config.LOGD && Email.DEBUG) {
178                Log.d(Email.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 (Config.LOGD && Email.DEBUG) {
202                Log.d(Email.LOG_TAG, e.toString());
203            }
204            throw new CertificateValidationException(e.getMessage(), e);
205        } catch (IOException ioe) {
206            if (Config.LOGD && Email.DEBUG) {
207                Log.d(Email.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 (Config.LOGD && Email.DEBUG) {
302            if (sensitiveReplacement != null && !Email.DEBUG_SENSITIVE) {
303                Log.d(Email.LOG_TAG, ">>> " + sensitiveReplacement);
304            } else {
305                Log.d(Email.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 && Config.LOGD && Email.DEBUG) {
334            Log.d(Email.LOG_TAG, "End of stream reached while trying to read line.");
335        }
336        String ret = sb.toString();
337        if (Config.LOGD) {
338            if (Email.DEBUG) {
339                Log.d(Email.LOG_TAG, "<<< " + ret);
340            }
341        }
342        return ret;
343    }
344
345
346}
347