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