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