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