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