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