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