SmtpSender.java revision f419287f22ae44f25e1ba1f757ec33c7941bbfa8
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 android.content.Context;
20import android.util.Base64;
21import android.util.Log;
22
23import com.android.email.mail.Sender;
24import com.android.email.mail.Transport;
25import com.android.email2.ui.MailActivityEmail;
26import com.android.emailcommon.Logging;
27import com.android.emailcommon.internet.Rfc822Output;
28import com.android.emailcommon.mail.Address;
29import com.android.emailcommon.mail.AuthenticationFailedException;
30import com.android.emailcommon.mail.CertificateValidationException;
31import com.android.emailcommon.mail.MessagingException;
32import com.android.emailcommon.provider.Account;
33import com.android.emailcommon.provider.EmailContent.Message;
34import com.android.emailcommon.provider.HostAuth;
35
36import java.io.IOException;
37import java.net.Inet6Address;
38import java.net.InetAddress;
39
40import javax.net.ssl.SSLException;
41
42/**
43 * This class handles all of the protocol-level aspects of sending messages via SMTP.
44 * TODO Remove dependence upon URI; there's no reason why we need it here
45 */
46public class SmtpSender extends Sender {
47
48    private final Context mContext;
49    private Transport mTransport;
50    private String mUsername;
51    private String mPassword;
52
53    /**
54     * Static named constructor.
55     */
56    public static Sender newInstance(Account account, Context context) throws MessagingException {
57        return new SmtpSender(context, account);
58    }
59
60    /**
61     * Creates a new sender for the given account.
62     */
63    private SmtpSender(Context context, Account account) throws MessagingException {
64        mContext = context;
65        HostAuth sendAuth = account.getOrCreateHostAuthSend(context);
66        if (sendAuth == null || !"smtp".equalsIgnoreCase(sendAuth.mProtocol)) {
67            throw new MessagingException("Unsupported protocol");
68        }
69        // defaults, which can be changed by security modifiers
70        int connectionSecurity = Transport.CONNECTION_SECURITY_NONE;
71        int defaultPort = 587;
72
73        // check for security flags and apply changes
74        if ((sendAuth.mFlags & HostAuth.FLAG_SSL) != 0) {
75            connectionSecurity = Transport.CONNECTION_SECURITY_SSL;
76            defaultPort = 465;
77        } else if ((sendAuth.mFlags & HostAuth.FLAG_TLS) != 0) {
78            connectionSecurity = Transport.CONNECTION_SECURITY_TLS;
79        }
80        boolean trustCertificates = ((sendAuth.mFlags & HostAuth.FLAG_TRUST_ALL) != 0);
81        int port = defaultPort;
82        if (sendAuth.mPort != HostAuth.PORT_UNKNOWN) {
83            port = sendAuth.mPort;
84        }
85        mTransport = new MailTransport("IMAP");
86        mTransport.setHost(sendAuth.mAddress);
87        mTransport.setPort(port);
88        mTransport.setSecurity(connectionSecurity, trustCertificates);
89
90        String[] userInfoParts = sendAuth.getLogin();
91        if (userInfoParts != null) {
92            mUsername = userInfoParts[0];
93            mPassword = userInfoParts[1];
94        }
95    }
96
97    /**
98     * For testing only.  Injects a different transport.  The transport should already be set
99     * up and ready to use.  Do not use for real code.
100     * @param testTransport The Transport to inject and use for all future communication.
101     */
102    /* package */ void setTransport(Transport testTransport) {
103        mTransport = testTransport;
104    }
105
106    @Override
107    public void open() throws MessagingException {
108        try {
109            mTransport.open();
110
111            // Eat the banner
112            executeSimpleCommand(null);
113
114            String localHost = "localhost";
115            // Try to get local address in the proper format.
116            InetAddress localAddress = mTransport.getLocalAddress();
117            if (localAddress != null) {
118                // Address Literal formatted in accordance to RFC2821 Sec. 4.1.3
119                StringBuilder sb = new StringBuilder();
120                sb.append('[');
121                if (localAddress instanceof Inet6Address) {
122                    sb.append("IPv6:");
123                }
124                sb.append(localAddress.getHostAddress());
125                sb.append(']');
126                localHost = sb.toString();
127            }
128            String result = executeSimpleCommand("EHLO " + localHost);
129
130            /*
131             * TODO may need to add code to fall back to HELO I switched it from
132             * using HELO on non STARTTLS connections because of AOL's mail
133             * server. It won't let you use AUTH without EHLO.
134             * We should really be paying more attention to the capabilities
135             * and only attempting auth if it's available, and warning the user
136             * if not.
137             */
138            if (mTransport.canTryTlsSecurity()) {
139                if (result.contains("STARTTLS")) {
140                    executeSimpleCommand("STARTTLS");
141                    mTransport.reopenTls();
142                    /*
143                     * Now resend the EHLO. Required by RFC2487 Sec. 5.2, and more specifically,
144                     * Exim.
145                     */
146                    result = executeSimpleCommand("EHLO " + localHost);
147                } else {
148                    if (MailActivityEmail.DEBUG) {
149                        Log.d(Logging.LOG_TAG, "TLS not supported but required");
150                    }
151                    throw new MessagingException(MessagingException.TLS_REQUIRED);
152                }
153            }
154
155            /*
156             * result contains the results of the EHLO in concatenated form
157             */
158            boolean authLoginSupported = result.matches(".*AUTH.*LOGIN.*$");
159            boolean authPlainSupported = result.matches(".*AUTH.*PLAIN.*$");
160
161            if (mUsername != null && mUsername.length() > 0 && mPassword != null
162                    && mPassword.length() > 0) {
163                if (authPlainSupported) {
164                    saslAuthPlain(mUsername, mPassword);
165                }
166                else if (authLoginSupported) {
167                    saslAuthLogin(mUsername, mPassword);
168                }
169                else {
170                    if (MailActivityEmail.DEBUG) {
171                        Log.d(Logging.LOG_TAG, "No valid authentication mechanism found.");
172                    }
173                    throw new MessagingException(MessagingException.AUTH_REQUIRED);
174                }
175            }
176        } catch (SSLException e) {
177            if (MailActivityEmail.DEBUG) {
178                Log.d(Logging.LOG_TAG, e.toString());
179            }
180            throw new CertificateValidationException(e.getMessage(), e);
181        } catch (IOException ioe) {
182            if (MailActivityEmail.DEBUG) {
183                Log.d(Logging.LOG_TAG, ioe.toString());
184            }
185            throw new MessagingException(MessagingException.IOERROR, ioe.toString());
186        }
187    }
188
189    @Override
190    public void sendMessage(long messageId) throws MessagingException {
191        close();
192        open();
193
194        Message message = Message.restoreMessageWithId(mContext, messageId);
195        if (message == null) {
196            throw new MessagingException("Trying to send non-existent message id="
197                    + Long.toString(messageId));
198        }
199        Address from = Address.unpackFirst(message.mFrom);
200        Address[] to = Address.unpack(message.mTo);
201        Address[] cc = Address.unpack(message.mCc);
202        Address[] bcc = Address.unpack(message.mBcc);
203
204        try {
205            executeSimpleCommand("MAIL FROM: " + "<" + from.getAddress() + ">");
206            for (Address address : to) {
207                executeSimpleCommand("RCPT TO: " + "<" + address.getAddress() + ">");
208            }
209            for (Address address : cc) {
210                executeSimpleCommand("RCPT TO: " + "<" + address.getAddress() + ">");
211            }
212            for (Address address : bcc) {
213                executeSimpleCommand("RCPT TO: " + "<" + address.getAddress() + ">");
214            }
215            executeSimpleCommand("DATA");
216            // TODO byte stuffing
217            Rfc822Output.writeTo(mContext, messageId,
218                    new EOLConvertingOutputStream(mTransport.getOutputStream()),
219                    false /* do not use smart reply */,
220                    false /* do not send BCC */);
221            executeSimpleCommand("\r\n.");
222        } catch (IOException ioe) {
223            throw new MessagingException("Unable to send message", ioe);
224        }
225    }
226
227    /**
228     * Close the protocol (and the transport below it).
229     *
230     * MUST NOT return any exceptions.
231     */
232    @Override
233    public void close() {
234        mTransport.close();
235    }
236
237    /**
238     * Send a single command and wait for a single response.  Handles responses that continue
239     * onto multiple lines.  Throws MessagingException if response code is 4xx or 5xx.  All traffic
240     * is logged (if debug logging is enabled) so do not use this function for user ID or password.
241     *
242     * @param command The command string to send to the server.
243     * @return Returns the response string from the server.
244     */
245    private String executeSimpleCommand(String command) throws IOException, MessagingException {
246        return executeSensitiveCommand(command, null);
247    }
248
249    /**
250     * Send a single command and wait for a single response.  Handles responses that continue
251     * onto multiple lines.  Throws MessagingException if response code is 4xx or 5xx.
252     *
253     * @param command The command string to send to the server.
254     * @param sensitiveReplacement If the command includes sensitive data (e.g. authentication)
255     * please pass a replacement string here (for logging).
256     * @return Returns the response string from the server.
257     */
258    private String executeSensitiveCommand(String command, String sensitiveReplacement)
259            throws IOException, MessagingException {
260        if (command != null) {
261            mTransport.writeLine(command, sensitiveReplacement);
262        }
263
264        String line = mTransport.readLine();
265
266        String result = line;
267
268        while (line.length() >= 4 && line.charAt(3) == '-') {
269            line = mTransport.readLine();
270            result += line.substring(3);
271        }
272
273        if (result.length() > 0) {
274            char c = result.charAt(0);
275            if ((c == '4') || (c == '5')) {
276                throw new MessagingException(result);
277            }
278        }
279
280        return result;
281    }
282
283
284//    C: AUTH LOGIN
285//    S: 334 VXNlcm5hbWU6
286//    C: d2VsZG9u
287//    S: 334 UGFzc3dvcmQ6
288//    C: dzNsZDBu
289//    S: 235 2.0.0 OK Authenticated
290//
291//    Lines 2-5 of the conversation contain base64-encoded information. The same conversation, with base64 strings decoded, reads:
292//
293//
294//    C: AUTH LOGIN
295//    S: 334 Username:
296//    C: weldon
297//    S: 334 Password:
298//    C: w3ld0n
299//    S: 235 2.0.0 OK Authenticated
300
301    private void saslAuthLogin(String username, String password) throws MessagingException,
302        AuthenticationFailedException, IOException {
303        try {
304            executeSimpleCommand("AUTH LOGIN");
305            executeSensitiveCommand(
306                    Base64.encodeToString(username.getBytes(), Base64.NO_WRAP),
307                    "/username redacted/");
308            executeSensitiveCommand(
309                    Base64.encodeToString(password.getBytes(), Base64.NO_WRAP),
310                    "/password redacted/");
311        }
312        catch (MessagingException me) {
313            if (me.getMessage().length() > 1 && me.getMessage().charAt(1) == '3') {
314                throw new AuthenticationFailedException(me.getMessage());
315            }
316            throw me;
317        }
318    }
319
320    private void saslAuthPlain(String username, String password) throws MessagingException,
321            AuthenticationFailedException, IOException {
322        byte[] data = ("\000" + username + "\000" + password).getBytes();
323        data = Base64.encode(data, Base64.NO_WRAP);
324        try {
325            executeSensitiveCommand("AUTH PLAIN " + new String(data), "AUTH PLAIN /redacted/");
326        }
327        catch (MessagingException me) {
328            if (me.getMessage().length() > 1 && me.getMessage().charAt(1) == '3') {
329                throw new AuthenticationFailedException(me.getMessage());
330            }
331            throw me;
332        }
333    }
334}
335