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 import android.util.Log; 22 23 import com.android.email.Email; 24 import com.android.email.mail.Sender; 25 import com.android.email.mail.Transport; 26 import com.android.emailcommon.Logging; 27 import com.android.emailcommon.internet.Rfc822Output; 28 import com.android.emailcommon.mail.Address; 29 import com.android.emailcommon.mail.AuthenticationFailedException; 30 import com.android.emailcommon.mail.CertificateValidationException; 31 import com.android.emailcommon.mail.MessagingException; 32 import com.android.emailcommon.provider.Account; 33 import com.android.emailcommon.provider.EmailContent.Message; 34 import com.android.emailcommon.provider.HostAuth; 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 * TODO Remove dependence upon URI; there's no reason why we need it here 45 */ 46 public 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 (Email.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 (Email.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 (Email.DEBUG) { 178 Log.d(Logging.LOG_TAG, e.toString()); 179 } 180 throw new CertificateValidationException(e.getMessage(), e); 181 } catch (IOException ioe) { 182 if (Email.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