SmtpSender.java revision 12b82d9374947c9268217f45befe8a74bd9b60d7
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 com.android.email.Email; 20import com.android.email.mail.Sender; 21import com.android.email.mail.Transport; 22import com.android.emailcommon.Logging; 23import com.android.emailcommon.internet.Rfc822Output; 24import com.android.emailcommon.mail.Address; 25import com.android.emailcommon.mail.AuthenticationFailedException; 26import com.android.emailcommon.mail.CertificateValidationException; 27import com.android.emailcommon.mail.MessagingException; 28import com.android.emailcommon.provider.EmailContent.Account; 29import com.android.emailcommon.provider.EmailContent.Message; 30import com.android.emailcommon.provider.HostAuth; 31 32import android.content.Context; 33import android.util.Log; 34import android.util.Base64; 35 36import java.io.IOException; 37import java.net.InetAddress; 38 39import javax.net.ssl.SSLException; 40 41/** 42 * This class handles all of the protocol-level aspects of sending messages via SMTP. 43 * TODO Remove dependence upon URI; there's no reason why we need it here 44 */ 45public class SmtpSender extends Sender { 46 47 private final Context mContext; 48 private Transport 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 private SmtpSender(Context context, Account account) throws MessagingException { 63 mContext = context; 64 HostAuth sendAuth = account.getOrCreateHostAuthSend(context); 65 if (sendAuth == null || !"smtp".equalsIgnoreCase(sendAuth.mProtocol)) { 66 throw new MessagingException("Unsupported protocol"); 67 } 68 // defaults, which can be changed by security modifiers 69 int connectionSecurity = Transport.CONNECTION_SECURITY_NONE; 70 int defaultPort = 587; 71 72 // check for security flags and apply changes 73 if ((sendAuth.mFlags & HostAuth.FLAG_SSL) != 0) { 74 connectionSecurity = Transport.CONNECTION_SECURITY_SSL; 75 defaultPort = 465; 76 } else if ((sendAuth.mFlags & HostAuth.FLAG_TLS) != 0) { 77 connectionSecurity = Transport.CONNECTION_SECURITY_TLS; 78 } 79 boolean trustCertificates = ((sendAuth.mFlags & HostAuth.FLAG_TRUST_ALL) != 0); 80 int port = defaultPort; 81 if (sendAuth.mPort != HostAuth.PORT_UNKNOWN) { 82 port = sendAuth.mPort; 83 } 84 mTransport = new MailTransport("IMAP"); 85 mTransport.setHost(sendAuth.mAddress); 86 mTransport.setPort(port); 87 mTransport.setSecurity(connectionSecurity, trustCertificates); 88 89 String[] userInfoParts = sendAuth.getLogin(); 90 if (userInfoParts != null) { 91 mUsername = userInfoParts[0]; 92 mPassword = userInfoParts[1]; 93 } 94 } 95 96 /** 97 * For testing only. Injects a different transport. The transport should already be set 98 * up and ready to use. Do not use for real code. 99 * @param testTransport The Transport to inject and use for all future communication. 100 */ 101 /* package */ void setTransport(Transport testTransport) { 102 mTransport = testTransport; 103 } 104 105 @Override 106 public void open() throws MessagingException { 107 try { 108 mTransport.open(); 109 110 // Eat the banner 111 executeSimpleCommand(null); 112 113 String localHost = "localhost"; 114 // Try to get local address in the X.X.X.X format. 115 InetAddress localAddress = mTransport.getLocalAddress(); 116 if (localAddress != null) { 117 localHost = localAddress.getHostAddress(); 118 } 119 String result = executeSimpleCommand("EHLO " + localHost); 120 121 /* 122 * TODO may need to add code to fall back to HELO I switched it from 123 * using HELO on non STARTTLS connections because of AOL's mail 124 * server. It won't let you use AUTH without EHLO. 125 * We should really be paying more attention to the capabilities 126 * and only attempting auth if it's available, and warning the user 127 * if not. 128 */ 129 if (mTransport.canTryTlsSecurity()) { 130 if (result.contains("-STARTTLS")) { 131 executeSimpleCommand("STARTTLS"); 132 mTransport.reopenTls(); 133 /* 134 * Now resend the EHLO. Required by RFC2487 Sec. 5.2, and more specifically, 135 * Exim. 136 */ 137 result = executeSimpleCommand("EHLO " + localHost); 138 } else { 139 if (Email.DEBUG) { 140 Log.d(Logging.LOG_TAG, "TLS not supported but required"); 141 } 142 throw new MessagingException(MessagingException.TLS_REQUIRED); 143 } 144 } 145 146 /* 147 * result contains the results of the EHLO in concatenated form 148 */ 149 boolean authLoginSupported = result.matches(".*AUTH.*LOGIN.*$"); 150 boolean authPlainSupported = result.matches(".*AUTH.*PLAIN.*$"); 151 152 if (mUsername != null && mUsername.length() > 0 && mPassword != null 153 && mPassword.length() > 0) { 154 if (authPlainSupported) { 155 saslAuthPlain(mUsername, mPassword); 156 } 157 else if (authLoginSupported) { 158 saslAuthLogin(mUsername, mPassword); 159 } 160 else { 161 if (Email.DEBUG) { 162 Log.d(Logging.LOG_TAG, "No valid authentication mechanism found."); 163 } 164 throw new MessagingException(MessagingException.AUTH_REQUIRED); 165 } 166 } 167 } catch (SSLException e) { 168 if (Email.DEBUG) { 169 Log.d(Logging.LOG_TAG, e.toString()); 170 } 171 throw new CertificateValidationException(e.getMessage(), e); 172 } catch (IOException ioe) { 173 if (Email.DEBUG) { 174 Log.d(Logging.LOG_TAG, ioe.toString()); 175 } 176 throw new MessagingException(MessagingException.IOERROR, ioe.toString()); 177 } 178 } 179 180 @Override 181 public void sendMessage(long messageId) throws MessagingException { 182 close(); 183 open(); 184 185 Message message = Message.restoreMessageWithId(mContext, messageId); 186 if (message == null) { 187 throw new MessagingException("Trying to send non-existent message id=" 188 + Long.toString(messageId)); 189 } 190 Address from = Address.unpackFirst(message.mFrom); 191 Address[] to = Address.unpack(message.mTo); 192 Address[] cc = Address.unpack(message.mCc); 193 Address[] bcc = Address.unpack(message.mBcc); 194 195 try { 196 executeSimpleCommand("MAIL FROM: " + "<" + from.getAddress() + ">"); 197 for (Address address : to) { 198 executeSimpleCommand("RCPT TO: " + "<" + address.getAddress() + ">"); 199 } 200 for (Address address : cc) { 201 executeSimpleCommand("RCPT TO: " + "<" + address.getAddress() + ">"); 202 } 203 for (Address address : bcc) { 204 executeSimpleCommand("RCPT TO: " + "<" + address.getAddress() + ">"); 205 } 206 executeSimpleCommand("DATA"); 207 // TODO byte stuffing 208 Rfc822Output.writeTo(mContext, messageId, 209 new EOLConvertingOutputStream(mTransport.getOutputStream()), 210 false /* do not use smart reply */, 211 false /* do not send BCC */); 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 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 */ 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 */ 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(); 256 257 String result = line; 258 259 while (line.length() >= 4 && line.charAt(3) == '-') { 260 line = mTransport.readLine(); 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 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 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} 326