Home | History | Annotate | Download | only in transport
      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 android.content.Context;
     20 import android.util.Base64;
     21 
     22 import com.android.email.mail.Sender;
     23 import com.android.email2.ui.MailActivityEmail;
     24 import com.android.emailcommon.Logging;
     25 import com.android.emailcommon.internet.Rfc822Output;
     26 import com.android.emailcommon.mail.Address;
     27 import com.android.emailcommon.mail.AuthenticationFailedException;
     28 import com.android.emailcommon.mail.CertificateValidationException;
     29 import com.android.emailcommon.mail.MessagingException;
     30 import com.android.emailcommon.provider.Account;
     31 import com.android.emailcommon.provider.EmailContent.Message;
     32 import com.android.emailcommon.provider.HostAuth;
     33 import com.android.emailcommon.utility.EOLConvertingOutputStream;
     34 import com.android.mail.utils.LogUtils;
     35 
     36 import java.io.IOException;
     37 import java.net.Inet6Address;
     38 import java.net.InetAddress;
     39 
     40 import javax.net.ssl.SSLException;
     41 
     42 /**
     43  * This class handles all of the protocol-level aspects of sending messages via SMTP.
     44  */
     45 public class SmtpSender extends Sender {
     46 
     47     private final Context mContext;
     48     private MailTransport mTransport;
     49     private String mUsername;
     50     private String mPassword;
     51 
     52     /**
     53      * Static named constructor.
     54      */
     55     public static Sender newInstance(Account account, Context context) throws MessagingException {
     56         return new SmtpSender(context, account);
     57     }
     58 
     59     /**
     60      * Creates a new sender for the given account.
     61      */
     62     public SmtpSender(Context context, Account account) {
     63         mContext = context;
     64         HostAuth sendAuth = account.getOrCreateHostAuthSend(context);
     65         mTransport = new MailTransport(context, "SMTP", sendAuth);
     66         String[] userInfoParts = sendAuth.getLogin();
     67         if (userInfoParts != null) {
     68             mUsername = userInfoParts[0];
     69             mPassword = userInfoParts[1];
     70         }
     71     }
     72 
     73     /**
     74      * For testing only.  Injects a different transport.  The transport should already be set
     75      * up and ready to use.  Do not use for real code.
     76      * @param testTransport The Transport to inject and use for all future communication.
     77      */
     78     public void setTransport(MailTransport testTransport) {
     79         mTransport = testTransport;
     80     }
     81 
     82     @Override
     83     public void open() throws MessagingException {
     84         try {
     85             mTransport.open();
     86 
     87             // Eat the banner
     88             executeSimpleCommand(null);
     89 
     90             String localHost = "localhost";
     91             // Try to get local address in the proper format.
     92             InetAddress localAddress = mTransport.getLocalAddress();
     93             if (localAddress != null) {
     94                 // Address Literal formatted in accordance to RFC2821 Sec. 4.1.3
     95                 StringBuilder sb = new StringBuilder();
     96                 sb.append('[');
     97                 if (localAddress instanceof Inet6Address) {
     98                     sb.append("IPv6:");
     99                 }
    100                 sb.append(localAddress.getHostAddress());
    101                 sb.append(']');
    102                 localHost = sb.toString();
    103             }
    104             String result = executeSimpleCommand("EHLO " + localHost);
    105 
    106             /*
    107              * TODO may need to add code to fall back to HELO I switched it from
    108              * using HELO on non STARTTLS connections because of AOL's mail
    109              * server. It won't let you use AUTH without EHLO.
    110              * We should really be paying more attention to the capabilities
    111              * and only attempting auth if it's available, and warning the user
    112              * if not.
    113              */
    114             if (mTransport.canTryTlsSecurity()) {
    115                 if (result.contains("STARTTLS")) {
    116                     executeSimpleCommand("STARTTLS");
    117                     mTransport.reopenTls();
    118                     /*
    119                      * Now resend the EHLO. Required by RFC2487 Sec. 5.2, and more specifically,
    120                      * Exim.
    121                      */
    122                     result = executeSimpleCommand("EHLO " + localHost);
    123                 } else {
    124                     if (MailActivityEmail.DEBUG) {
    125                         LogUtils.d(Logging.LOG_TAG, "TLS not supported but required");
    126                     }
    127                     throw new MessagingException(MessagingException.TLS_REQUIRED);
    128                 }
    129             }
    130 
    131             /*
    132              * result contains the results of the EHLO in concatenated form
    133              */
    134             boolean authLoginSupported = result.matches(".*AUTH.*LOGIN.*$");
    135             boolean authPlainSupported = result.matches(".*AUTH.*PLAIN.*$");
    136 
    137             if (mUsername != null && mUsername.length() > 0 && mPassword != null
    138                     && mPassword.length() > 0) {
    139                 if (authPlainSupported) {
    140                     saslAuthPlain(mUsername, mPassword);
    141                 }
    142                 else if (authLoginSupported) {
    143                     saslAuthLogin(mUsername, mPassword);
    144                 }
    145                 else {
    146                     if (MailActivityEmail.DEBUG) {
    147                         LogUtils.d(Logging.LOG_TAG, "No valid authentication mechanism found.");
    148                     }
    149                     throw new MessagingException(MessagingException.AUTH_REQUIRED);
    150                 }
    151             }
    152         } catch (SSLException e) {
    153             if (MailActivityEmail.DEBUG) {
    154                 LogUtils.d(Logging.LOG_TAG, e.toString());
    155             }
    156             throw new CertificateValidationException(e.getMessage(), e);
    157         } catch (IOException ioe) {
    158             if (MailActivityEmail.DEBUG) {
    159                 LogUtils.d(Logging.LOG_TAG, ioe.toString());
    160             }
    161             throw new MessagingException(MessagingException.IOERROR, ioe.toString());
    162         }
    163     }
    164 
    165     @Override
    166     public void sendMessage(long messageId) throws MessagingException {
    167         close();
    168         open();
    169 
    170         Message message = Message.restoreMessageWithId(mContext, messageId);
    171         if (message == null) {
    172             throw new MessagingException("Trying to send non-existent message id="
    173                     + Long.toString(messageId));
    174         }
    175         Address from = Address.unpackFirst(message.mFrom);
    176         Address[] to = Address.unpack(message.mTo);
    177         Address[] cc = Address.unpack(message.mCc);
    178         Address[] bcc = Address.unpack(message.mBcc);
    179 
    180         try {
    181             executeSimpleCommand("MAIL FROM:" + "<" + from.getAddress() + ">");
    182             for (Address address : to) {
    183                 executeSimpleCommand("RCPT TO:" + "<" + address.getAddress().trim() + ">");
    184             }
    185             for (Address address : cc) {
    186                 executeSimpleCommand("RCPT TO:" + "<" + address.getAddress().trim() + ">");
    187             }
    188             for (Address address : bcc) {
    189                 executeSimpleCommand("RCPT TO:" + "<" + address.getAddress().trim() + ">");
    190             }
    191             executeSimpleCommand("DATA");
    192             // TODO byte stuffing
    193             Rfc822Output.writeTo(mContext, message,
    194                     new EOLConvertingOutputStream(mTransport.getOutputStream()),
    195                     false /* do not use smart reply */,
    196                     false /* do not send BCC */,
    197                     null  /* attachments are in the message itself */);
    198             executeSimpleCommand("\r\n.");
    199         } catch (IOException ioe) {
    200             throw new MessagingException("Unable to send message", ioe);
    201         }
    202     }
    203 
    204     /**
    205      * Close the protocol (and the transport below it).
    206      *
    207      * MUST NOT return any exceptions.
    208      */
    209     @Override
    210     public void close() {
    211         mTransport.close();
    212     }
    213 
    214     /**
    215      * Send a single command and wait for a single response.  Handles responses that continue
    216      * onto multiple lines.  Throws MessagingException if response code is 4xx or 5xx.  All traffic
    217      * is logged (if debug logging is enabled) so do not use this function for user ID or password.
    218      *
    219      * @param command The command string to send to the server.
    220      * @return Returns the response string from the server.
    221      */
    222     private String executeSimpleCommand(String command) throws IOException, MessagingException {
    223         return executeSensitiveCommand(command, null);
    224     }
    225 
    226     /**
    227      * Send a single command and wait for a single response.  Handles responses that continue
    228      * onto multiple lines.  Throws MessagingException if response code is 4xx or 5xx.
    229      *
    230      * @param command The command string to send to the server.
    231      * @param sensitiveReplacement If the command includes sensitive data (e.g. authentication)
    232      * please pass a replacement string here (for logging).
    233      * @return Returns the response string from the server.
    234      */
    235     private String executeSensitiveCommand(String command, String sensitiveReplacement)
    236             throws IOException, MessagingException {
    237         if (command != null) {
    238             mTransport.writeLine(command, sensitiveReplacement);
    239         }
    240 
    241         String line = mTransport.readLine(true);
    242 
    243         String result = line;
    244 
    245         while (line.length() >= 4 && line.charAt(3) == '-') {
    246             line = mTransport.readLine(true);
    247             result += line.substring(3);
    248         }
    249 
    250         if (result.length() > 0) {
    251             char c = result.charAt(0);
    252             if ((c == '4') || (c == '5')) {
    253                 throw new MessagingException(result);
    254             }
    255         }
    256 
    257         return result;
    258     }
    259 
    260 
    261 //    C: AUTH LOGIN
    262 //    S: 334 VXNlcm5hbWU6
    263 //    C: d2VsZG9u
    264 //    S: 334 UGFzc3dvcmQ6
    265 //    C: dzNsZDBu
    266 //    S: 235 2.0.0 OK Authenticated
    267 //
    268 //    Lines 2-5 of the conversation contain base64-encoded information. The same conversation, with base64 strings decoded, reads:
    269 //
    270 //
    271 //    C: AUTH LOGIN
    272 //    S: 334 Username:
    273 //    C: weldon
    274 //    S: 334 Password:
    275 //    C: w3ld0n
    276 //    S: 235 2.0.0 OK Authenticated
    277 
    278     private void saslAuthLogin(String username, String password) throws MessagingException,
    279         AuthenticationFailedException, IOException {
    280         try {
    281             executeSimpleCommand("AUTH LOGIN");
    282             executeSensitiveCommand(
    283                     Base64.encodeToString(username.getBytes(), Base64.NO_WRAP),
    284                     "/username redacted/");
    285             executeSensitiveCommand(
    286                     Base64.encodeToString(password.getBytes(), Base64.NO_WRAP),
    287                     "/password redacted/");
    288         }
    289         catch (MessagingException me) {
    290             if (me.getMessage().length() > 1 && me.getMessage().charAt(1) == '3') {
    291                 throw new AuthenticationFailedException(me.getMessage());
    292             }
    293             throw me;
    294         }
    295     }
    296 
    297     private void saslAuthPlain(String username, String password) throws MessagingException,
    298             AuthenticationFailedException, IOException {
    299         byte[] data = ("\000" + username + "\000" + password).getBytes();
    300         data = Base64.encode(data, Base64.NO_WRAP);
    301         try {
    302             executeSensitiveCommand("AUTH PLAIN " + new String(data), "AUTH PLAIN /redacted/");
    303         }
    304         catch (MessagingException me) {
    305             if (me.getMessage().length() > 1 && me.getMessage().charAt(1) == '3') {
    306                 throw new AuthenticationFailedException(me.getMessage());
    307             }
    308             throw me;
    309         }
    310     }
    311 }
    312