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 com.android.email.Email; 20 import com.android.email.codec.binary.Base64; 21 import com.android.email.mail.Address; 22 import com.android.email.mail.AuthenticationFailedException; 23 import com.android.email.mail.CertificateValidationException; 24 import com.android.email.mail.MessagingException; 25 import com.android.email.mail.Sender; 26 import com.android.email.mail.Transport; 27 import com.android.email.provider.EmailContent.Message; 28 29 import android.content.Context; 30 import android.util.Config; 31 import android.util.Log; 32 33 import java.io.IOException; 34 import java.net.InetAddress; 35 import java.net.URI; 36 import java.net.URISyntaxException; 37 38 import javax.net.ssl.SSLException; 39 40 /** 41 * This class handles all of the protocol-level aspects of sending messages via SMTP. 42 */ 43 public class SmtpSender extends Sender { 44 45 Context mContext; 46 private Transport mTransport; 47 String mUsername; 48 String mPassword; 49 50 /** 51 * Static named constructor. 52 */ newInstance(Context context, String uri)53 public static Sender newInstance(Context context, String uri) throws MessagingException { 54 return new SmtpSender(context, uri); 55 } 56 57 /** 58 * Allowed formats for the Uri: 59 * smtp://user:password@server:port 60 * smtp+tls+://user:password@server:port 61 * smtp+tls+trustallcerts://user:password@server:port 62 * smtp+ssl+://user:password@server:port 63 * smtp+ssl+trustallcerts://user:password@server:port 64 * 65 * @param uriString the Uri containing information to configure this sender 66 */ SmtpSender(Context context, String uriString)67 private SmtpSender(Context context, String uriString) throws MessagingException { 68 mContext = context; 69 URI uri; 70 try { 71 uri = new URI(uriString); 72 } catch (URISyntaxException use) { 73 throw new MessagingException("Invalid SmtpTransport URI", use); 74 } 75 76 String scheme = uri.getScheme(); 77 if (scheme == null || !scheme.startsWith("smtp")) { 78 throw new MessagingException("Unsupported protocol"); 79 } 80 // defaults, which can be changed by security modifiers 81 int connectionSecurity = Transport.CONNECTION_SECURITY_NONE; 82 int defaultPort = 587; 83 // check for security modifiers and apply changes 84 if (scheme.contains("+ssl")) { 85 connectionSecurity = Transport.CONNECTION_SECURITY_SSL; 86 defaultPort = 465; 87 } else if (scheme.contains("+tls")) { 88 connectionSecurity = Transport.CONNECTION_SECURITY_TLS; 89 } 90 boolean trustCertificates = scheme.contains("+trustallcerts"); 91 92 mTransport = new MailTransport("SMTP"); 93 mTransport.setUri(uri, defaultPort); 94 mTransport.setSecurity(connectionSecurity, trustCertificates); 95 96 String[] userInfoParts = mTransport.getUserInfoParts(); 97 if (userInfoParts != null) { 98 mUsername = userInfoParts[0]; 99 if (userInfoParts.length > 1) { 100 mPassword = userInfoParts[1]; 101 } 102 } 103 } 104 105 /** 106 * For testing only. Injects a different transport. The transport should already be set 107 * up and ready to use. Do not use for real code. 108 * @param testTransport The Transport to inject and use for all future communication. 109 */ setTransport(Transport testTransport)110 /* package */ void setTransport(Transport testTransport) { 111 mTransport = testTransport; 112 } 113 114 @Override open()115 public void open() throws MessagingException { 116 try { 117 mTransport.open(); 118 119 // Eat the banner 120 executeSimpleCommand(null); 121 122 String localHost = "localhost"; 123 try { 124 InetAddress localAddress = InetAddress.getLocalHost(); 125 localHost = localAddress.getHostName(); 126 } catch (Exception e) { 127 if (Config.LOGD && Email.DEBUG) { 128 Log.d(Email.LOG_TAG, "Unable to look up localhost"); 129 } 130 } 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 (Config.LOGD && Email.DEBUG) { 153 Log.d(Email.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 (Config.LOGD && Email.DEBUG) { 175 Log.d(Email.LOG_TAG, "No valid authentication mechanism found."); 176 } 177 throw new MessagingException(MessagingException.AUTH_REQUIRED); 178 } 179 } 180 } catch (SSLException e) { 181 if (Config.LOGD && Email.DEBUG) { 182 Log.d(Email.LOG_TAG, e.toString()); 183 } 184 throw new CertificateValidationException(e.getMessage(), e); 185 } catch (IOException ioe) { 186 if (Config.LOGD && Email.DEBUG) { 187 Log.d(Email.LOG_TAG, ioe.toString()); 188 } 189 throw new MessagingException(MessagingException.IOERROR, ioe.toString()); 190 } 191 } 192 193 @Override sendMessage(long messageId)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()), true, false); 223 executeSimpleCommand("\r\n."); 224 } catch (IOException ioe) { 225 throw new MessagingException("Unable to send message", ioe); 226 } 227 } 228 229 /** 230 * Close the protocol (and the transport below it). 231 * 232 * MUST NOT return any exceptions. 233 */ 234 @Override close()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 */ executeSimpleCommand(String command)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 */ executeSensitiveCommand(String command, String sensitiveReplacement)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 if (result.length() > 0) { 276 char c = result.charAt(0); 277 if ((c == '4') || (c == '5')) { 278 throw new MessagingException(result); 279 } 280 } 281 282 return result; 283 } 284 285 286 // C: AUTH LOGIN 287 // S: 334 VXNlcm5hbWU6 288 // C: d2VsZG9u 289 // S: 334 UGFzc3dvcmQ6 290 // C: dzNsZDBu 291 // S: 235 2.0.0 OK Authenticated 292 // 293 // Lines 2-5 of the conversation contain base64-encoded information. The same conversation, with base64 strings decoded, reads: 294 // 295 // 296 // C: AUTH LOGIN 297 // S: 334 Username: 298 // C: weldon 299 // S: 334 Password: 300 // C: w3ld0n 301 // S: 235 2.0.0 OK Authenticated 302 saslAuthLogin(String username, String password)303 private void saslAuthLogin(String username, String password) throws MessagingException, 304 AuthenticationFailedException, IOException { 305 try { 306 executeSimpleCommand("AUTH LOGIN"); 307 executeSensitiveCommand(new String(Base64.encodeBase64(username.getBytes())), 308 "/username redacted/"); 309 executeSensitiveCommand(new String(Base64.encodeBase64(password.getBytes())), 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 saslAuthPlain(String username, String password)320 private void saslAuthPlain(String username, String password) throws MessagingException, 321 AuthenticationFailedException, IOException { 322 byte[] data = ("\000" + username + "\000" + password).getBytes(); 323 data = new Base64().encode(data); 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