SmtpSender.java revision 96c5af40d639d629267794f4f0338a267ff94ce5
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.codec.binary.Base64; 21import com.android.email.mail.Address; 22import com.android.email.mail.AuthenticationFailedException; 23import com.android.email.mail.CertificateValidationException; 24import com.android.email.mail.Message; 25import com.android.email.mail.MessagingException; 26import com.android.email.mail.Sender; 27import com.android.email.mail.Transport; 28import com.android.email.mail.Message.RecipientType; 29 30import android.util.Config; 31import android.util.Log; 32 33import java.io.BufferedOutputStream; 34import java.io.IOException; 35import java.net.InetAddress; 36import java.net.URI; 37import java.net.URISyntaxException; 38 39import javax.net.ssl.SSLException; 40 41/** 42 * This class handles all of the protocol-level aspects of sending messages via SMTP. 43 */ 44public class SmtpSender extends Sender { 45 46 public static final int CONNECTION_SECURITY_NONE = 0; 47 public static final int CONNECTION_SECURITY_TLS_OPTIONAL = 1; 48 public static final int CONNECTION_SECURITY_TLS_REQUIRED = 2; 49 public static final int CONNECTION_SECURITY_SSL_REQUIRED = 3; 50 public static final int CONNECTION_SECURITY_SSL_OPTIONAL = 4; 51 52 private Transport mTransport; 53 String mUsername; 54 String mPassword; 55 56 /** 57 * Allowed formats for the Uri: 58 * smtp://user:password@server:port CONNECTION_SECURITY_NONE 59 * smtp+tls://user:password@server:port CONNECTION_SECURITY_TLS_OPTIONAL 60 * smtp+tls+://user:password@server:port CONNECTION_SECURITY_TLS_REQUIRED 61 * smtp+ssl+://user:password@server:port CONNECTION_SECURITY_SSL_REQUIRED 62 * smtp+ssl://user:password@server:port CONNECTION_SECURITY_SSL_OPTIONAL 63 * 64 * @param uriString the Uri containing information to configure this sender 65 */ 66 public SmtpSender(String uriString) throws MessagingException { 67 URI uri; 68 try { 69 uri = new URI(uriString); 70 } catch (URISyntaxException use) { 71 throw new MessagingException("Invalid SmtpTransport URI", use); 72 } 73 74 String scheme = uri.getScheme(); 75 int connectionSecurity = Transport.CONNECTION_SECURITY_NONE; 76 int defaultPort = -1; 77 if (scheme.equals("smtp")) { 78 connectionSecurity = CONNECTION_SECURITY_NONE; 79 defaultPort = 25; 80 } else if (scheme.equals("smtp+tls")) { 81 connectionSecurity = CONNECTION_SECURITY_TLS_OPTIONAL; 82 defaultPort = 25; 83 } else if (scheme.equals("smtp+tls+")) { 84 connectionSecurity = CONNECTION_SECURITY_TLS_REQUIRED; 85 defaultPort = 25; 86 } else if (scheme.equals("smtp+ssl+")) { 87 connectionSecurity = CONNECTION_SECURITY_SSL_REQUIRED; 88 defaultPort = 465; 89 } else if (scheme.equals("smtp+ssl")) { 90 connectionSecurity = CONNECTION_SECURITY_SSL_OPTIONAL; 91 defaultPort = 465; 92 } else { 93 throw new MessagingException("Unsupported protocol"); 94 } 95 96 mTransport = new MailTransport("SMTP"); 97 mTransport.setUri(uri, defaultPort); 98 mTransport.setSecurity(connectionSecurity); 99 100 String[] userInfoParts = mTransport.getUserInfoParts(); 101 if (userInfoParts != null) { 102 mUsername = userInfoParts[0]; 103 if (userInfoParts.length > 1) { 104 mPassword = userInfoParts[1]; 105 } 106 } 107 } 108 109 /** 110 * For testing only. Injects a different transport. The transport should already be set 111 * up and ready to use. Do not use for real code. 112 * @param testTransport The Transport to inject and use for all future communication. 113 */ 114 /* package */ void setTransport(Transport testTransport) { 115 mTransport = testTransport; 116 } 117 118 public void open() throws MessagingException { 119 try { 120 mTransport.open(); 121 122 // Eat the banner 123 executeSimpleCommand(null); 124 125 String localHost = "localhost"; 126 try { 127 InetAddress localAddress = InetAddress.getLocalHost(); 128 localHost = localAddress.getHostName(); 129 } catch (Exception e) { 130 if (Config.LOGD && Email.DEBUG) { 131 Log.d(Email.LOG_TAG, "Unable to look up localhost"); 132 } 133 } 134 135 String result = executeSimpleCommand("EHLO " + localHost); 136 137 /* 138 * TODO may need to add code to fall back to HELO I switched it from 139 * using HELO on non STARTTLS connections because of AOL's mail 140 * server. It won't let you use AUTH without EHLO. 141 * We should really be paying more attention to the capabilities 142 * and only attempting auth if it's available, and warning the user 143 * if not. 144 */ 145 if (mTransport.canTryTlsSecurity()) { 146 if (result.contains("-STARTTLS")) { 147 executeSimpleCommand("STARTTLS"); 148 mTransport.reopenTls(); 149 /* 150 * Now resend the EHLO. Required by RFC2487 Sec. 5.2, and more specifically, 151 * Exim. 152 */ 153 result = executeSimpleCommand("EHLO " + localHost); 154 } else if (mTransport.getSecurity() == 155 Transport.CONNECTION_SECURITY_TLS_REQUIRED) { 156 if (Config.LOGD && Email.DEBUG) { 157 Log.d(Email.LOG_TAG, "TLS not supported but required"); 158 } 159 throw new MessagingException(MessagingException.TLS_REQUIRED); 160 } 161 } 162 163 /* 164 * result contains the results of the EHLO in concatenated form 165 */ 166 boolean authLoginSupported = result.matches(".*AUTH.*LOGIN.*$"); 167 boolean authPlainSupported = result.matches(".*AUTH.*PLAIN.*$"); 168 169 if (mUsername != null && mUsername.length() > 0 && mPassword != null 170 && mPassword.length() > 0) { 171 if (authPlainSupported) { 172 saslAuthPlain(mUsername, mPassword); 173 } 174 else if (authLoginSupported) { 175 saslAuthLogin(mUsername, mPassword); 176 } 177 else { 178 if (Config.LOGD && Email.DEBUG) { 179 Log.d(Email.LOG_TAG, "No valid authentication mechanism found."); 180 } 181 throw new MessagingException(MessagingException.AUTH_REQUIRED); 182 } 183 } 184 } catch (SSLException e) { 185 if (Config.LOGD && Email.DEBUG) { 186 Log.d(Email.LOG_TAG, e.toString()); 187 } 188 throw new CertificateValidationException(e.getMessage(), e); 189 } catch (IOException ioe) { 190 if (Config.LOGD && Email.DEBUG) { 191 Log.d(Email.LOG_TAG, ioe.toString()); 192 } 193 throw new MessagingException(MessagingException.IOERROR, ioe.toString()); 194 } 195 } 196 197 public void sendMessage(Message message) throws MessagingException { 198 close(); 199 open(); 200 Address[] from = message.getFrom(); 201 202 try { 203 executeSimpleCommand("MAIL FROM: " + "<" + from[0].getAddress() + ">"); 204 for (Address address : message.getRecipients(RecipientType.TO)) { 205 executeSimpleCommand("RCPT TO: " + "<" + address.getAddress() + ">"); 206 } 207 for (Address address : message.getRecipients(RecipientType.CC)) { 208 executeSimpleCommand("RCPT TO: " + "<" + address.getAddress() + ">"); 209 } 210 for (Address address : message.getRecipients(RecipientType.BCC)) { 211 executeSimpleCommand("RCPT TO: " + "<" + address.getAddress() + ">"); 212 } 213 message.setRecipients(RecipientType.BCC, null); 214 executeSimpleCommand("DATA"); 215 // TODO byte stuffing 216 // TODO most of the MIME writeTo functions layer on *additional* buffering 217 // streams, making this one possibly not-necessary. Need to get to the bottom 218 // of that. 219 // TODO Also, need to be absolutely positively sure that flush() is called 220 // on the wrappered outputs before sending the final \r\n via the regular mOut. 221 message.writeTo( 222 new EOLConvertingOutputStream( 223 new BufferedOutputStream(mTransport.getOutputStream(), 1024))); 224 executeSimpleCommand("\r\n."); 225 } catch (IOException ioe) { 226 throw new MessagingException("Unable to send message", ioe); 227 } 228 } 229 230 /** 231 * Close the protocol (and the transport below it). 232 * 233 * MUST NOT return any exceptions. 234 */ 235 public void close() { 236 mTransport.close(); 237 } 238 239 /** 240 * Send a single command and wait for a single response. Handles responses that continue 241 * onto multiple lines. Throws MessagingException if response code is 4xx or 5xx. All traffic 242 * is logged (if debug logging is enabled) so do not use this function for user ID or password. 243 * 244 * @param command The command string to send to the server. 245 * @return Returns the response string from the server. 246 */ 247 private String executeSimpleCommand(String command) throws IOException, MessagingException { 248 return executeSensitiveCommand(command, null); 249 } 250 251 /** 252 * Send a single command and wait for a single response. Handles responses that continue 253 * onto multiple lines. Throws MessagingException if response code is 4xx or 5xx. 254 * 255 * @param command The command string to send to the server. 256 * @param sensitiveReplacement If the command includes sensitive data (e.g. authentication) 257 * please pass a replacement string here (for logging). 258 * @return Returns the response string from the server. 259 */ 260 private String executeSensitiveCommand(String command, String sensitiveReplacement) 261 throws IOException, MessagingException { 262 if (command != null) { 263 mTransport.writeLine(command, sensitiveReplacement); 264 } 265 266 String line = mTransport.readLine(); 267 268 String result = line; 269 270 while (line.length() >= 4 && line.charAt(3) == '-') { 271 line = mTransport.readLine(); 272 result += line.substring(3); 273 } 274 275 char c = result.charAt(0); 276 if ((c == '4') || (c == '5')) { 277 throw new MessagingException(result); 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(new String(Base64.encodeBase64(username.getBytes())), 306 "/username redacted/"); 307 executeSensitiveCommand(new String(Base64.encodeBase64(password.getBytes())), 308 "/password redacted/"); 309 } 310 catch (MessagingException me) { 311 if (me.getMessage().length() > 1 && me.getMessage().charAt(1) == '3') { 312 throw new AuthenticationFailedException(me.getMessage()); 313 } 314 throw me; 315 } 316 } 317 318 private void saslAuthPlain(String username, String password) throws MessagingException, 319 AuthenticationFailedException, IOException { 320 byte[] data = ("\000" + username + "\000" + password).getBytes(); 321 data = new Base64().encode(data); 322 try { 323 executeSensitiveCommand("AUTH PLAIN " + new String(data), "AUTH PLAIN /redacted/"); 324 } 325 catch (MessagingException me) { 326 if (me.getMessage().length() > 1 && me.getMessage().charAt(1) == '3') { 327 throw new AuthenticationFailedException(me.getMessage()); 328 } 329 throw me; 330 } 331 } 332} 333