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