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