MailTransport.java revision e4a7cc440f081ef9c4375a2bd2f82680cc11b152
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; 23import com.android.email.mail.store.TrustManagerFactory; 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.InetSocketAddress; 34import java.net.Socket; 35import java.net.SocketAddress; 36import java.net.SocketException; 37import java.net.URI; 38import java.security.GeneralSecurityException; 39import java.security.SecureRandom; 40 41import javax.net.ssl.SSLContext; 42import javax.net.ssl.SSLException; 43import javax.net.ssl.TrustManager; 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 String mDebugLabel; 56 57 private String mHost; 58 private int mPort; 59 private String[] mUserInfoParts; 60 private int mConnectionSecurity; 61 private boolean mTrustCertificates; 62 63 private Socket mSocket; 64 private InputStream mIn; 65 private OutputStream mOut; 66 67 /** 68 * Simple constructor for starting from scratch. Call setUri() and setSecurity() to 69 * complete the configuration. 70 * @param debugLabel Label used for Log.d calls 71 */ 72 public MailTransport(String debugLabel) { 73 super(); 74 mDebugLabel = debugLabel; 75 } 76 77 /** 78 * Get a new transport, using an existing one as a model. The new transport is configured as if 79 * setUri() and setSecurity() have been called, but not opened or connected in any way. 80 * @return a new Transport ready to open() 81 */ 82 public Transport newInstanceWithConfiguration() { 83 MailTransport newObject = new MailTransport(mDebugLabel); 84 85 newObject.mDebugLabel = mDebugLabel; 86 newObject.mHost = mHost; 87 newObject.mPort = mPort; 88 if (mUserInfoParts != null) { 89 newObject.mUserInfoParts = mUserInfoParts.clone(); 90 } 91 newObject.mConnectionSecurity = mConnectionSecurity; 92 newObject.mTrustCertificates = mTrustCertificates; 93 return newObject; 94 } 95 96 public void setUri(URI uri, int defaultPort) { 97 mHost = uri.getHost(); 98 99 mPort = defaultPort; 100 if (uri.getPort() != -1) { 101 mPort = uri.getPort(); 102 } 103 104 if (uri.getUserInfo() != null) { 105 mUserInfoParts = uri.getUserInfo().split(":", 2); 106 } 107 108 } 109 110 public String[] getUserInfoParts() { 111 return mUserInfoParts; 112 } 113 114 public String getHost() { 115 return mHost; 116 } 117 118 public int getPort() { 119 return mPort; 120 } 121 122 public void setSecurity(int connectionSecurity, boolean trustAllCertificates) { 123 mConnectionSecurity = connectionSecurity; 124 mTrustCertificates = trustAllCertificates; 125 } 126 127 public int getSecurity() { 128 return mConnectionSecurity; 129 } 130 131 public boolean canTrySslSecurity() { 132 return mConnectionSecurity == CONNECTION_SECURITY_SSL; 133 } 134 135 public boolean canTryTlsSecurity() { 136 return mConnectionSecurity == Transport.CONNECTION_SECURITY_TLS; 137 } 138 139 public boolean canTrustAllCertificates() { 140 return mTrustCertificates; 141 } 142 143 /** 144 * Attempts to open a connection using the Uri supplied for connection parameters. Will attempt 145 * an SSL connection if indicated. 146 */ 147 public void open() throws MessagingException, CertificateValidationException { 148 if (Config.LOGD && Email.DEBUG) { 149 Log.d(Email.LOG_TAG, "*** " + mDebugLabel + " open " + 150 getHost() + ":" + String.valueOf(getPort())); 151 } 152 153 try { 154 SocketAddress socketAddress = new InetSocketAddress(getHost(), getPort()); 155 if (canTrySslSecurity()) { 156 SSLContext sslContext = SSLContext.getInstance("TLS"); 157 sslContext.init(null, new TrustManager[] { 158 TrustManagerFactory.get(getHost(), !canTrustAllCertificates()) 159 }, new SecureRandom()); 160 mSocket = sslContext.getSocketFactory().createSocket(); 161 } else { 162 mSocket = new Socket(); 163 } 164 mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT); 165 mIn = new BufferedInputStream(mSocket.getInputStream(), 1024); 166 mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512); 167 168 } catch (SSLException e) { 169 if (Config.LOGD && Email.DEBUG) { 170 Log.d(Email.LOG_TAG, e.toString()); 171 } 172 throw new CertificateValidationException(e.getMessage(), e); 173 } catch (GeneralSecurityException gse) { 174 if (Config.LOGD && Email.DEBUG) { 175 Log.d(Email.LOG_TAG, gse.toString()); 176 } 177 throw new MessagingException(MessagingException.GENERAL_SECURITY, gse.toString()); 178 } catch (IOException ioe) { 179 if (Config.LOGD && Email.DEBUG) { 180 Log.d(Email.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 * TODO should we explicitly close the old socket? This seems funky to abandon it. 190 */ 191 public void reopenTls() throws MessagingException { 192 try { 193 SSLContext sslContext = SSLContext.getInstance("TLS"); 194 sslContext.init(null, new TrustManager[] { 195 TrustManagerFactory.get(getHost(), !canTrustAllCertificates()) 196 }, new SecureRandom()); 197 mSocket = sslContext.getSocketFactory().createSocket(mSocket, getHost(), getPort(), 198 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 (Config.LOGD && Email.DEBUG) { 205 Log.d(Email.LOG_TAG, e.toString()); 206 } 207 throw new CertificateValidationException(e.getMessage(), e); 208 } catch (GeneralSecurityException gse) { 209 if (Config.LOGD && Email.DEBUG) { 210 Log.d(Email.LOG_TAG, gse.toString()); 211 } 212 throw new MessagingException(MessagingException.GENERAL_SECURITY, gse.toString()); 213 } catch (IOException ioe) { 214 if (Config.LOGD && Email.DEBUG) { 215 Log.d(Email.LOG_TAG, ioe.toString()); 216 } 217 throw new MessagingException(MessagingException.IOERROR, ioe.toString()); 218 } 219 } 220 221 /** 222 * Set the socket timeout. 223 * @param timeoutMilliseconds the read timeout value if greater than {@code 0}, or 224 * {@code 0} for an infinite timeout. 225 */ 226 public void setSoTimeout(int timeoutMilliseconds) throws SocketException { 227 mSocket.setSoTimeout(timeoutMilliseconds); 228 } 229 230 public boolean isOpen() { 231 return (mIn != null && mOut != null && 232 mSocket != null && mSocket.isConnected() && !mSocket.isClosed()); 233 } 234 235 /** 236 * Close the connection. MUST NOT return any exceptions - must be "best effort" and safe. 237 */ 238 public void close() { 239 try { 240 mIn.close(); 241 } catch (Exception e) { 242 // May fail if the connection is already closed. 243 } 244 try { 245 mOut.close(); 246 } catch (Exception e) { 247 // May fail if the connection is already closed. 248 } 249 try { 250 mSocket.close(); 251 } catch (Exception e) { 252 // May fail if the connection is already closed. 253 } 254 mIn = null; 255 mOut = null; 256 mSocket = null; 257 } 258 259 public InputStream getInputStream() { 260 return mIn; 261 } 262 263 public OutputStream getOutputStream() { 264 return mOut; 265 } 266 267 /** 268 * Writes a single line to the server using \r\n termination. 269 */ 270 public void writeLine(String s, String sensitiveReplacement) throws IOException { 271 if (Config.LOGD && Email.DEBUG) { 272 if (sensitiveReplacement != null && !Email.DEBUG_SENSITIVE) { 273 Log.d(Email.LOG_TAG, ">>> " + sensitiveReplacement); 274 } else { 275 Log.d(Email.LOG_TAG, ">>> " + s); 276 } 277 } 278 279 OutputStream out = getOutputStream(); 280 out.write(s.getBytes()); 281 out.write('\r'); 282 out.write('\n'); 283 out.flush(); 284 } 285 286 /** 287 * Reads a single line from the server, using either \r\n or \n as the delimiter. The 288 * delimiter char(s) are not included in the result. 289 */ 290 public String readLine() throws IOException { 291 StringBuffer sb = new StringBuffer(); 292 InputStream in = getInputStream(); 293 int d; 294 while ((d = in.read()) != -1) { 295 if (((char)d) == '\r') { 296 continue; 297 } else if (((char)d) == '\n') { 298 break; 299 } else { 300 sb.append((char)d); 301 } 302 } 303 if (d == -1 && Config.LOGD && Email.DEBUG) { 304 Log.d(Email.LOG_TAG, "End of stream reached while trying to read line."); 305 } 306 String ret = sb.toString(); 307 if (Config.LOGD) { 308 if (Email.DEBUG) { 309 Log.d(Email.LOG_TAG, "<<< " + ret); 310 } 311 } 312 return ret; 313 } 314 315 316} 317