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