/src/com/android/email/mail/transport/SmtpSender.java
Java | 353 lines | 233 code | 33 blank | 87 comment | 49 complexity | f1f28f91b90414a5a9abaf034076b3e6 MD5 | raw file
- /*
- * 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.mail.transport;
- import android.content.Context;
- import android.util.Base64;
- import com.android.email.DebugUtils;
- import com.android.email.mail.Sender;
- import com.android.email.mail.internet.AuthenticationCache;
- 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.provider.Account;
- import com.android.emailcommon.provider.Credential;
- import com.android.emailcommon.provider.EmailContent.Message;
- import com.android.emailcommon.provider.HostAuth;
- import com.android.emailcommon.utility.EOLConvertingOutputStream;
- import com.android.mail.utils.LogUtils;
- 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.
- */
- public class SmtpSender extends Sender {
- private final Context mContext;
- private MailTransport mTransport;
- private Account mAccount;
- private String mUsername;
- private String mPassword;
- private boolean mUseOAuth;
- /**
- * Static named constructor.
- */
- public static Sender newInstance(Account account, Context context) throws MessagingException {
- return new SmtpSender(context, account);
- }
- /**
- * Creates a new sender for the given account.
- */
- public SmtpSender(Context context, Account account) {
- mContext = context;
- mAccount = account;
- HostAuth sendAuth = account.getOrCreateHostAuthSend(context);
- mTransport = new MailTransport(context, "SMTP", sendAuth);
- String[] userInfoParts = sendAuth.getLogin();
- mUsername = userInfoParts[0];
- mPassword = userInfoParts[1];
- Credential cred = sendAuth.getCredential(context);
- if (cred != null) {
- mUseOAuth = true;
- }
- }
- /**
- * 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.
- */
- public void setTransport(MailTransport testTransport) {
- mTransport = testTransport;
- }
- @Override
- 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 (DebugUtils.DEBUG) {
- LogUtils.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.*$");
- boolean authOAuthSupported = result.matches(".*AUTH.*XOAUTH2.*$");
- if (mUseOAuth) {
- if (!authOAuthSupported) {
- LogUtils.w(Logging.LOG_TAG, "OAuth requested, but not supported.");
- throw new MessagingException(MessagingException.OAUTH_NOT_SUPPORTED);
- }
- saslAuthOAuth(mUsername);
- } else if (mUsername != null && mUsername.length() > 0 && mPassword != null
- && mPassword.length() > 0) {
- if (authPlainSupported) {
- saslAuthPlain(mUsername, mPassword);
- }
- else if (authLoginSupported) {
- saslAuthLogin(mUsername, mPassword);
- }
- else {
- LogUtils.w(Logging.LOG_TAG, "No valid authentication mechanism found.");
- throw new MessagingException(MessagingException.AUTH_REQUIRED);
- }
- } else {
- // It is acceptable to hvae no authentication at all for SMTP.
- }
- } catch (SSLException e) {
- if (DebugUtils.DEBUG) {
- LogUtils.d(Logging.LOG_TAG, e.toString());
- }
- throw new CertificateValidationException(e.getMessage(), e);
- } catch (IOException ioe) {
- if (DebugUtils.DEBUG) {
- LogUtils.d(Logging.LOG_TAG, ioe.toString());
- }
- throw new MessagingException(MessagingException.IOERROR, ioe.toString());
- }
- }
- @Override
- 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.firstAddress(message.mFrom);
- Address[] to = Address.fromHeader(message.mTo);
- Address[] cc = Address.fromHeader(message.mCc);
- Address[] bcc = Address.fromHeader(message.mBcc);
- try {
- executeSimpleCommand("MAIL FROM:" + "<" + from.getAddress() + ">");
- for (Address address : to) {
- executeSimpleCommand("RCPT TO:" + "<" + address.getAddress().trim() + ">");
- }
- for (Address address : cc) {
- executeSimpleCommand("RCPT TO:" + "<" + address.getAddress().trim() + ">");
- }
- for (Address address : bcc) {
- executeSimpleCommand("RCPT TO:" + "<" + address.getAddress().trim() + ">");
- }
- executeSimpleCommand("DATA");
- // TODO byte stuffing
- Rfc822Output.writeTo(mContext, message,
- new EOLConvertingOutputStream(mTransport.getOutputStream()),
- false /* do not use smart reply */,
- false /* do not send BCC */,
- null /* attachments are in the message itself */);
- 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.
- */
- @Override
- 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(true);
- String result = line;
- while (line.length() >= 4 && line.charAt(3) == '-') {
- line = mTransport.readLine(true);
- 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;
- }
- }
- private void saslAuthOAuth(String username) throws MessagingException,
- AuthenticationFailedException, IOException {
- final AuthenticationCache cache = AuthenticationCache.getInstance();
- String accessToken = cache.retrieveAccessToken(mContext, mAccount);
- try {
- saslAuthOAuth(username, accessToken);
- } catch (AuthenticationFailedException e) {
- accessToken = cache.refreshAccessToken(mContext, mAccount);
- saslAuthOAuth(username, accessToken);
- }
- }
- private void saslAuthOAuth(final String username, final String accessToken) throws IOException,
- MessagingException {
- final String authPhrase = "user=" + username + '\001' + "auth=Bearer " + accessToken +
- '\001' + '\001';
- byte[] data = Base64.encode(authPhrase.getBytes(), Base64.NO_WRAP);
- try {
- executeSensitiveCommand("AUTH XOAUTH2 " + new String(data),
- "AUTH XOAUTH2 /redacted/");
- } catch (MessagingException me) {
- if (me.getMessage().length() > 1 && me.getMessage().charAt(1) == '3') {
- throw new AuthenticationFailedException(me.getMessage());
- }
- throw me;
- }
- }
- }