Merge "Remove duplicate classes; trim addresses in RCPT TO:" into jb-ub-mail
This commit is contained in:
commit
a98f024b40
@ -30,7 +30,7 @@ import android.os.RemoteException;
|
|||||||
import android.util.Base64;
|
import android.util.Base64;
|
||||||
import android.util.Log;
|
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.Logging;
|
||||||
import com.android.emailcommon.TrafficFlags;
|
import com.android.emailcommon.TrafficFlags;
|
||||||
import com.android.emailcommon.internet.MimeUtility;
|
import com.android.emailcommon.internet.MimeUtility;
|
||||||
@ -2254,7 +2254,7 @@ public class Imap2SyncService extends AbstractSyncService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
SmtpSender sender = new SmtpSender(mContext, account, mUserLog);
|
SmtpSender sender = new SmtpSender(mContext, account);
|
||||||
|
|
||||||
// 3. loop through the available messages and send them
|
// 3. loop through the available messages and send them
|
||||||
while (c.moveToNext()) {
|
while (c.moveToNext()) {
|
||||||
|
@ -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.
|
|
||||||
*
|
|
||||||
* <p>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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -61,12 +61,9 @@ public class SmtpSender extends Sender {
|
|||||||
/**
|
/**
|
||||||
* Creates a new sender for the given account.
|
* Creates a new sender for the given account.
|
||||||
*/
|
*/
|
||||||
private SmtpSender(Context context, Account account) throws MessagingException {
|
public SmtpSender(Context context, Account account) {
|
||||||
mContext = context;
|
mContext = context;
|
||||||
HostAuth sendAuth = account.getOrCreateHostAuthSend(context);
|
HostAuth sendAuth = account.getOrCreateHostAuthSend(context);
|
||||||
if (sendAuth == null || !"smtp".equalsIgnoreCase(sendAuth.mProtocol)) {
|
|
||||||
throw new MessagingException("Unsupported protocol");
|
|
||||||
}
|
|
||||||
mTransport = new MailTransport(context, "SMTP", sendAuth);
|
mTransport = new MailTransport(context, "SMTP", sendAuth);
|
||||||
String[] userInfoParts = sendAuth.getLogin();
|
String[] userInfoParts = sendAuth.getLogin();
|
||||||
if (userInfoParts != null) {
|
if (userInfoParts != null) {
|
||||||
@ -185,13 +182,13 @@ public class SmtpSender extends Sender {
|
|||||||
try {
|
try {
|
||||||
executeSimpleCommand("MAIL FROM: " + "<" + from.getAddress() + ">");
|
executeSimpleCommand("MAIL FROM: " + "<" + from.getAddress() + ">");
|
||||||
for (Address address : to) {
|
for (Address address : to) {
|
||||||
executeSimpleCommand("RCPT TO: " + "<" + address.getAddress() + ">");
|
executeSimpleCommand("RCPT TO: " + "<" + address.getAddress().trim() + ">");
|
||||||
}
|
}
|
||||||
for (Address address : cc) {
|
for (Address address : cc) {
|
||||||
executeSimpleCommand("RCPT TO: " + "<" + address.getAddress() + ">");
|
executeSimpleCommand("RCPT TO: " + "<" + address.getAddress().trim() + ">");
|
||||||
}
|
}
|
||||||
for (Address address : bcc) {
|
for (Address address : bcc) {
|
||||||
executeSimpleCommand("RCPT TO: " + "<" + address.getAddress() + ">");
|
executeSimpleCommand("RCPT TO: " + "<" + address.getAddress().trim() + ">");
|
||||||
}
|
}
|
||||||
executeSimpleCommand("DATA");
|
executeSimpleCommand("DATA");
|
||||||
// TODO byte stuffing
|
// TODO byte stuffing
|
||||||
|
Loading…
Reference in New Issue
Block a user