diff --git a/src/com/android/email/imap2/Imap2SyncService.java b/src/com/android/email/imap2/Imap2SyncService.java index b0e0ab006..400354543 100644 --- a/src/com/android/email/imap2/Imap2SyncService.java +++ b/src/com/android/email/imap2/Imap2SyncService.java @@ -30,7 +30,7 @@ import android.os.RemoteException; import android.util.Base64; import android.util.Log; -import com.android.email.imap2.smtp.SmtpSender; +import com.android.email.mail.transport.SmtpSender; import com.android.emailcommon.Logging; import com.android.emailcommon.TrafficFlags; import com.android.emailcommon.internet.MimeUtility; @@ -2254,7 +2254,7 @@ public class Imap2SyncService extends AbstractSyncService { return; } - SmtpSender sender = new SmtpSender(mContext, account, mUserLog); + SmtpSender sender = new SmtpSender(mContext, account); // 3. loop through the available messages and send them while (c.moveToNext()) { diff --git a/src/com/android/email/imap2/smtp/MailTransport.java b/src/com/android/email/imap2/smtp/MailTransport.java deleted file mode 100644 index 2dd80c40d..000000000 --- a/src/com/android/email/imap2/smtp/MailTransport.java +++ /dev/null @@ -1,325 +0,0 @@ -/* - * Copyright (C) 2008 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.email.imap2.smtp; - -import com.android.emailcommon.Logging; -import com.android.emailcommon.mail.CertificateValidationException; -import com.android.emailcommon.mail.MessagingException; -import com.android.emailcommon.mail.Transport; -import com.android.emailcommon.provider.HostAuth; -import com.android.emailcommon.utility.SSLUtils; - -import android.content.Context; -import android.util.Log; - -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.Socket; -import java.net.SocketAddress; -import java.net.SocketException; - -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.SSLException; -import javax.net.ssl.SSLPeerUnverifiedException; -import javax.net.ssl.SSLSession; -import javax.net.ssl.SSLSocket; - -/** - * This class implements the common aspects of "transport", one layer below the - * specific wire protocols such as POP3, IMAP, or SMTP. - */ -public class MailTransport implements Transport { - - // TODO protected eventually - /*protected*/ public static final int SOCKET_CONNECT_TIMEOUT = 10000; - /*protected*/ public static final int SOCKET_READ_TIMEOUT = 60000; - - private static final HostnameVerifier HOSTNAME_VERIFIER = - HttpsURLConnection.getDefaultHostnameVerifier(); - - private final HostAuth mHostAuth; - private final Context mContext; - - private Socket mSocket; - private InputStream mIn; - private OutputStream mOut; - private boolean mLog = true; // STOPSHIP Don't ship with this set to true - - - public MailTransport(Context context, boolean log, HostAuth hostAuth) { - super(); - mContext = context; - mHostAuth = hostAuth; - } - - /** - * Returns a new transport, using the current transport as a model. The new transport is - * configured identically (as if {@link #setSecurity(int, boolean)}, {@link #setPort(int)} - * and {@link #setHost(String)} were invoked), but not opened or connected in any way. - */ - @Override - public Transport clone() { - return new MailTransport(mContext, mLog, mHostAuth); - } - - @Override - public String getHost() { - return mHostAuth.mAddress; - } - - @Override - public int getPort() { - return mHostAuth.mPort; - } - - @Override - public boolean canTrySslSecurity() { - return (mHostAuth.mFlags & HostAuth.FLAG_SSL) != 0; - } - - @Override - public boolean canTryTlsSecurity() { - return (mHostAuth.mFlags & HostAuth.FLAG_TLS) != 0; - } - - @Override - public boolean canTrustAllCertificates() { - return (mHostAuth.mFlags & HostAuth.FLAG_TRUST_ALL) != 0; - } - - /** - * Attempts to open a connection using the Uri supplied for connection parameters. Will attempt - * an SSL connection if indicated. - */ - @Override - public void open() throws MessagingException, CertificateValidationException { - if (mLog) { - Log.d(Logging.LOG_TAG, "*** SMTP open " + - getHost() + ":" + String.valueOf(getPort())); - } - - try { - SocketAddress socketAddress = new InetSocketAddress(getHost(), getPort()); - if (canTrySslSecurity()) { - mSocket = SSLUtils.getSSLSocketFactory( - mContext, mHostAuth, canTrustAllCertificates()).createSocket(); - } else { - mSocket = new Socket(); - } - mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT); - // After the socket connects to an SSL server, confirm that the hostname is as expected - if (canTrySslSecurity() && !canTrustAllCertificates()) { - verifyHostname(mSocket, getHost()); - } - mIn = new BufferedInputStream(mSocket.getInputStream(), 1024); - mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512); - - } catch (SSLException e) { - if (mLog) { - Log.d(Logging.LOG_TAG, e.toString()); - } - throw new CertificateValidationException(e.getMessage(), e); - } catch (IOException ioe) { - if (mLog) { - Log.d(Logging.LOG_TAG, ioe.toString()); - } - throw new MessagingException(MessagingException.IOERROR, ioe.toString()); - } - } - - /** - * Attempts to reopen a TLS connection using the Uri supplied for connection parameters. - * - * NOTE: No explicit hostname verification is required here, because it's handled automatically - * by the call to createSocket(). - * - * TODO should we explicitly close the old socket? This seems funky to abandon it. - */ - @Override - public void reopenTls() throws MessagingException { - try { - mSocket = SSLUtils.getSSLSocketFactory(mContext, mHostAuth, canTrustAllCertificates()) - .createSocket(mSocket, getHost(), getPort(), true); - mSocket.setSoTimeout(SOCKET_READ_TIMEOUT); - mIn = new BufferedInputStream(mSocket.getInputStream(), 1024); - mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512); - - } catch (SSLException e) { - if (mLog) { - Log.d(Logging.LOG_TAG, e.toString()); - } - throw new CertificateValidationException(e.getMessage(), e); - } catch (IOException ioe) { - if (mLog) { - Log.d(Logging.LOG_TAG, ioe.toString()); - } - throw new MessagingException(MessagingException.IOERROR, ioe.toString()); - } - } - - /** - * Lightweight version of SSLCertificateSocketFactory.verifyHostname, which provides this - * service but is not in the public API. - * - * Verify the hostname of the certificate used by the other end of a - * connected socket. You MUST call this if you did not supply a hostname - * to SSLCertificateSocketFactory.createSocket(). It is harmless to call this method - * redundantly if the hostname has already been verified. - * - *
Wildcard certificates are allowed to verify any matching hostname, - * so "foo.bar.example.com" is verified if the peer has a certificate - * for "*.example.com". - * - * @param socket An SSL socket which has been connected to a server - * @param hostname The expected hostname of the remote server - * @throws IOException if something goes wrong handshaking with the server - * @throws SSLPeerUnverifiedException if the server cannot prove its identity - */ - private void verifyHostname(Socket socket, String hostname) throws IOException { - // The code at the start of OpenSSLSocketImpl.startHandshake() - // ensures that the call is idempotent, so we can safely call it. - SSLSocket ssl = (SSLSocket) socket; - ssl.startHandshake(); - - SSLSession session = ssl.getSession(); - if (session == null) { - throw new SSLException("Cannot verify SSL socket without session"); - } - // TODO: Instead of reporting the name of the server we think we're connecting to, - // we should be reporting the bad name in the certificate. Unfortunately this is buried - // in the verifier code and is not available in the verifier API, and extracting the - // CN & alts is beyond the scope of this patch. - if (!HOSTNAME_VERIFIER.verify(hostname, session)) { - throw new SSLPeerUnverifiedException( - "Certificate hostname not useable for server: " + hostname); - } - } - - /** - * Set the socket timeout. - * @param timeoutMilliseconds the read timeout value if greater than {@code 0}, or - * {@code 0} for an infinite timeout. - */ - @Override - public void setSoTimeout(int timeoutMilliseconds) throws SocketException { - mSocket.setSoTimeout(timeoutMilliseconds); - } - - @Override - public boolean isOpen() { - return (mIn != null && mOut != null && - mSocket != null && mSocket.isConnected() && !mSocket.isClosed()); - } - - /** - * Close the connection. MUST NOT return any exceptions - must be "best effort" and safe. - */ - @Override - public void close() { - try { - mIn.close(); - } catch (Exception e) { - // May fail if the connection is already closed. - } - try { - mOut.close(); - } catch (Exception e) { - // May fail if the connection is already closed. - } - try { - mSocket.close(); - } catch (Exception e) { - // May fail if the connection is already closed. - } - mIn = null; - mOut = null; - mSocket = null; - } - - @Override - public InputStream getInputStream() { - return mIn; - } - - @Override - public OutputStream getOutputStream() { - return mOut; - } - - /** - * Writes a single line to the server using \r\n termination. - */ - @Override - public void writeLine(String s, String sensitiveReplacement) throws IOException { - if (mLog) { - if (sensitiveReplacement != null && !Logging.DEBUG_SENSITIVE) { - Log.d(Logging.LOG_TAG, ">>> " + sensitiveReplacement); - } else { - Log.d(Logging.LOG_TAG, ">>> " + s); - } - } - - OutputStream out = getOutputStream(); - out.write(s.getBytes()); - out.write('\r'); - out.write('\n'); - out.flush(); - } - - /** - * Reads a single line from the server, using either \r\n or \n as the delimiter. The - * delimiter char(s) are not included in the result. - */ - @Override - public String readLine() throws IOException { - StringBuffer sb = new StringBuffer(); - InputStream in = getInputStream(); - int d; - while ((d = in.read()) != -1) { - if (((char)d) == '\r') { - continue; - } else if (((char)d) == '\n') { - break; - } else { - sb.append((char)d); - } - } - if (d == -1 && mLog) { - Log.d(Logging.LOG_TAG, "End of stream reached while trying to read line."); - } - String ret = sb.toString(); - if (mLog) { - Log.d(Logging.LOG_TAG, "<<< " + ret); - } - return ret; - } - - @Override - public InetAddress getLocalAddress() { - if (isOpen()) { - return mSocket.getLocalAddress(); - } else { - return null; - } - } -} diff --git a/src/com/android/email/imap2/smtp/SmtpSender.java b/src/com/android/email/imap2/smtp/SmtpSender.java deleted file mode 100644 index a90aea649..000000000 --- a/src/com/android/email/imap2/smtp/SmtpSender.java +++ /dev/null @@ -1,306 +0,0 @@ -/* - * Copyright (C) 2008 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.email.imap2.smtp; - -import android.content.Context; -import android.util.Base64; -import android.util.Log; - -import com.android.emailcommon.Logging; -import com.android.emailcommon.internet.Rfc822Output; -import com.android.emailcommon.mail.Address; -import com.android.emailcommon.mail.AuthenticationFailedException; -import com.android.emailcommon.mail.CertificateValidationException; -import com.android.emailcommon.mail.MessagingException; -import com.android.emailcommon.mail.Transport; -import com.android.emailcommon.provider.Account; -import com.android.emailcommon.provider.EmailContent.Message; -import com.android.emailcommon.provider.HostAuth; -import com.android.emailcommon.utility.EOLConvertingOutputStream; - -import java.io.IOException; -import java.net.Inet6Address; -import java.net.InetAddress; - -import javax.net.ssl.SSLException; - -/** - * This class handles all of the protocol-level aspects of sending messages via SMTP. - * TODO Remove dependence upon URI; there's no reason why we need it here - */ -public class SmtpSender { - - private static final int DEFAULT_SMTP_PORT = 587; - private static final int DEFAULT_SMTP_SSL_PORT = 465; - - private final Context mContext; - private Transport mTransport; - private String mUsername; - private String mPassword; - private boolean mLog; - - /** - * Creates a new sender for the given account. - */ - public SmtpSender(Context context, Account account, boolean log) { - mContext = context; - mLog = log; - HostAuth sendAuth = account.getOrCreateHostAuthSend(context); - mTransport = new MailTransport(context, mLog, sendAuth); - - String[] userInfoParts = sendAuth.getLogin(); - if (userInfoParts != null) { - mUsername = userInfoParts[0]; - mPassword = userInfoParts[1]; - } - } - - /** - * For testing only. Injects a different transport. The transport should already be set - * up and ready to use. Do not use for real code. - * @param testTransport The Transport to inject and use for all future communication. - */ - /* package */ void setTransport(Transport testTransport) { - mTransport = testTransport; - } - - public void open() throws MessagingException { - try { - mTransport.open(); - - // Eat the banner - executeSimpleCommand(null); - - String localHost = "localhost"; - // Try to get local address in the proper format. - InetAddress localAddress = mTransport.getLocalAddress(); - if (localAddress != null) { - // Address Literal formatted in accordance to RFC2821 Sec. 4.1.3 - StringBuilder sb = new StringBuilder(); - sb.append('['); - if (localAddress instanceof Inet6Address) { - sb.append("IPv6:"); - } - sb.append(localAddress.getHostAddress()); - sb.append(']'); - localHost = sb.toString(); - } - String result = executeSimpleCommand("EHLO " + localHost); - - /* - * TODO may need to add code to fall back to HELO I switched it from - * using HELO on non STARTTLS connections because of AOL's mail - * server. It won't let you use AUTH without EHLO. - * We should really be paying more attention to the capabilities - * and only attempting auth if it's available, and warning the user - * if not. - */ - if (mTransport.canTryTlsSecurity()) { - if (result.contains("STARTTLS")) { - executeSimpleCommand("STARTTLS"); - mTransport.reopenTls(); - /* - * Now resend the EHLO. Required by RFC2487 Sec. 5.2, and more specifically, - * Exim. - */ - result = executeSimpleCommand("EHLO " + localHost); - } else { - if (mLog) { - Log.d(Logging.LOG_TAG, "TLS not supported but required"); - } - throw new MessagingException(MessagingException.TLS_REQUIRED); - } - } - - /* - * result contains the results of the EHLO in concatenated form - */ - boolean authLoginSupported = result.matches(".*AUTH.*LOGIN.*$"); - boolean authPlainSupported = result.matches(".*AUTH.*PLAIN.*$"); - - if (mUsername != null && mUsername.length() > 0 && mPassword != null - && mPassword.length() > 0) { - if (authPlainSupported) { - saslAuthPlain(mUsername, mPassword); - } - else if (authLoginSupported) { - saslAuthLogin(mUsername, mPassword); - } - else { - if (mLog) { - Log.d(Logging.LOG_TAG, "No valid authentication mechanism found."); - } - throw new MessagingException(MessagingException.AUTH_REQUIRED); - } - } - } catch (SSLException e) { - if (mLog) { - Log.d(Logging.LOG_TAG, e.toString()); - } - throw new CertificateValidationException(e.getMessage(), e); - } catch (IOException ioe) { - if (mLog) { - Log.d(Logging.LOG_TAG, ioe.toString()); - } - throw new MessagingException(MessagingException.IOERROR, ioe.toString()); - } - } - - public void sendMessage(long messageId) throws MessagingException { - close(); - open(); - - Message message = Message.restoreMessageWithId(mContext, messageId); - if (message == null) { - throw new MessagingException("Trying to send non-existent message id=" - + Long.toString(messageId)); - } - Address from = Address.unpackFirst(message.mFrom); - Address[] to = Address.unpack(message.mTo); - Address[] cc = Address.unpack(message.mCc); - Address[] bcc = Address.unpack(message.mBcc); - - try { - executeSimpleCommand("MAIL FROM: " + "<" + from.getAddress() + ">"); - for (Address address : to) { - executeSimpleCommand("RCPT TO: " + "<" + address.getAddress() + ">"); - } - for (Address address : cc) { - executeSimpleCommand("RCPT TO: " + "<" + address.getAddress() + ">"); - } - for (Address address : bcc) { - executeSimpleCommand("RCPT TO: " + "<" + address.getAddress() + ">"); - } - executeSimpleCommand("DATA"); - // TODO byte stuffing - Rfc822Output.writeTo(mContext, messageId, - new EOLConvertingOutputStream(mTransport.getOutputStream()), - false /* do not use smart reply */, - false /* do not send BCC */); - executeSimpleCommand("\r\n."); - } catch (IOException ioe) { - throw new MessagingException("Unable to send message", ioe); - } - } - - /** - * Close the protocol (and the transport below it). - * - * MUST NOT return any exceptions. - */ - public void close() { - mTransport.close(); - } - - /** - * Send a single command and wait for a single response. Handles responses that continue - * onto multiple lines. Throws MessagingException if response code is 4xx or 5xx. All traffic - * is logged (if debug logging is enabled) so do not use this function for user ID or password. - * - * @param command The command string to send to the server. - * @return Returns the response string from the server. - */ - private String executeSimpleCommand(String command) throws IOException, MessagingException { - return executeSensitiveCommand(command, null); - } - - /** - * Send a single command and wait for a single response. Handles responses that continue - * onto multiple lines. Throws MessagingException if response code is 4xx or 5xx. - * - * @param command The command string to send to the server. - * @param sensitiveReplacement If the command includes sensitive data (e.g. authentication) - * please pass a replacement string here (for logging). - * @return Returns the response string from the server. - */ - private String executeSensitiveCommand(String command, String sensitiveReplacement) - throws IOException, MessagingException { - if (command != null) { - mTransport.writeLine(command, sensitiveReplacement); - } - - String line = mTransport.readLine(); - - String result = line; - - while (line.length() >= 4 && line.charAt(3) == '-') { - line = mTransport.readLine(); - result += line.substring(3); - } - - if (result.length() > 0) { - char c = result.charAt(0); - if ((c == '4') || (c == '5')) { - throw new MessagingException(result); - } - } - - return result; - } - - -// C: AUTH LOGIN -// S: 334 VXNlcm5hbWU6 -// C: d2VsZG9u -// S: 334 UGFzc3dvcmQ6 -// C: dzNsZDBu -// S: 235 2.0.0 OK Authenticated -// -// Lines 2-5 of the conversation contain base64-encoded information. The same conversation, with base64 strings decoded, reads: -// -// -// C: AUTH LOGIN -// S: 334 Username: -// C: weldon -// S: 334 Password: -// C: w3ld0n -// S: 235 2.0.0 OK Authenticated - - private void saslAuthLogin(String username, String password) throws MessagingException, - AuthenticationFailedException, IOException { - try { - executeSimpleCommand("AUTH LOGIN"); - executeSensitiveCommand( - Base64.encodeToString(username.getBytes(), Base64.NO_WRAP), - "/username redacted/"); - executeSensitiveCommand( - Base64.encodeToString(password.getBytes(), Base64.NO_WRAP), - "/password redacted/"); - } - catch (MessagingException me) { - if (me.getMessage().length() > 1 && me.getMessage().charAt(1) == '3') { - throw new AuthenticationFailedException(me.getMessage()); - } - throw me; - } - } - - private void saslAuthPlain(String username, String password) throws MessagingException, - AuthenticationFailedException, IOException { - byte[] data = ("\000" + username + "\000" + password).getBytes(); - data = Base64.encode(data, Base64.NO_WRAP); - try { - executeSensitiveCommand("AUTH PLAIN " + new String(data), "AUTH PLAIN /redacted/"); - } - catch (MessagingException me) { - if (me.getMessage().length() > 1 && me.getMessage().charAt(1) == '3') { - throw new AuthenticationFailedException(me.getMessage()); - } - throw me; - } - } -} diff --git a/src/com/android/email/mail/transport/SmtpSender.java b/src/com/android/email/mail/transport/SmtpSender.java index 9472480d3..ce4a8f844 100644 --- a/src/com/android/email/mail/transport/SmtpSender.java +++ b/src/com/android/email/mail/transport/SmtpSender.java @@ -61,12 +61,9 @@ public class SmtpSender extends Sender { /** * Creates a new sender for the given account. */ - private SmtpSender(Context context, Account account) throws MessagingException { + public SmtpSender(Context context, Account account) { mContext = context; HostAuth sendAuth = account.getOrCreateHostAuthSend(context); - if (sendAuth == null || !"smtp".equalsIgnoreCase(sendAuth.mProtocol)) { - throw new MessagingException("Unsupported protocol"); - } mTransport = new MailTransport(context, "SMTP", sendAuth); String[] userInfoParts = sendAuth.getLogin(); if (userInfoParts != null) { @@ -185,13 +182,13 @@ public class SmtpSender extends Sender { try { executeSimpleCommand("MAIL FROM: " + "<" + from.getAddress() + ">"); for (Address address : to) { - executeSimpleCommand("RCPT TO: " + "<" + address.getAddress() + ">"); + executeSimpleCommand("RCPT TO: " + "<" + address.getAddress().trim() + ">"); } for (Address address : cc) { - executeSimpleCommand("RCPT TO: " + "<" + address.getAddress() + ">"); + executeSimpleCommand("RCPT TO: " + "<" + address.getAddress().trim() + ">"); } for (Address address : bcc) { - executeSimpleCommand("RCPT TO: " + "<" + address.getAddress() + ">"); + executeSimpleCommand("RCPT TO: " + "<" + address.getAddress().trim() + ">"); } executeSimpleCommand("DATA"); // TODO byte stuffing