Home | History | Annotate | Download | only in store
      1 /*
      2  * Copyright (C) 2015 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 package com.android.voicemail.impl.mail.store;
     17 
     18 import android.util.ArraySet;
     19 import android.util.Base64;
     20 import com.android.voicemail.impl.OmtpEvents;
     21 import com.android.voicemail.impl.VvmLog;
     22 import com.android.voicemail.impl.mail.AuthenticationFailedException;
     23 import com.android.voicemail.impl.mail.CertificateValidationException;
     24 import com.android.voicemail.impl.mail.MailTransport;
     25 import com.android.voicemail.impl.mail.MessagingException;
     26 import com.android.voicemail.impl.mail.store.ImapStore.ImapException;
     27 import com.android.voicemail.impl.mail.store.imap.DigestMd5Utils;
     28 import com.android.voicemail.impl.mail.store.imap.ImapConstants;
     29 import com.android.voicemail.impl.mail.store.imap.ImapResponse;
     30 import com.android.voicemail.impl.mail.store.imap.ImapResponseParser;
     31 import com.android.voicemail.impl.mail.store.imap.ImapUtility;
     32 import com.android.voicemail.impl.mail.utils.LogUtils;
     33 import java.io.IOException;
     34 import java.util.ArrayList;
     35 import java.util.List;
     36 import java.util.Map;
     37 import java.util.Set;
     38 import java.util.concurrent.atomic.AtomicInteger;
     39 import javax.net.ssl.SSLException;
     40 
     41 /** A cacheable class that stores the details for a single IMAP connection. */
     42 public class ImapConnection {
     43   private final String TAG = "ImapConnection";
     44 
     45   private String loginPhrase;
     46   private ImapStore imapStore;
     47   private MailTransport transport;
     48   private ImapResponseParser parser;
     49   private Set<String> capabilities = new ArraySet<>();
     50 
     51   static final String IMAP_REDACTED_LOG = "[IMAP command redacted]";
     52 
     53   /**
     54    * Next tag to use. All connections associated to the same ImapStore instance share the same
     55    * counter to make tests simpler. (Some of the tests involve multiple connections but only have a
     56    * single counter to track the tag.)
     57    */
     58   private final AtomicInteger nextCommandTag = new AtomicInteger(0);
     59 
     60   ImapConnection(ImapStore store) {
     61     setStore(store);
     62   }
     63 
     64   void setStore(ImapStore store) {
     65     // TODO: maybe we should throw an exception if the connection is not closed here,
     66     // if it's not currently closed, then we won't reopen it, so if the credentials have
     67     // changed, the connection will not be reestablished.
     68     imapStore = store;
     69     loginPhrase = null;
     70   }
     71 
     72   /**
     73    * Generates and returns the phrase to be used for authentication. This will be a LOGIN with
     74    * username and password.
     75    *
     76    * @return the login command string to sent to the IMAP server
     77    */
     78   String getLoginPhrase() {
     79     if (loginPhrase == null) {
     80       if (imapStore.getUsername() != null && imapStore.getPassword() != null) {
     81         // build the LOGIN string once (instead of over-and-over again.)
     82         // apply the quoting here around the built-up password
     83         loginPhrase =
     84             ImapConstants.LOGIN
     85                 + " "
     86                 + imapStore.getUsername()
     87                 + " "
     88                 + ImapUtility.imapQuoted(imapStore.getPassword());
     89       }
     90     }
     91     return loginPhrase;
     92   }
     93 
     94   public void open() throws IOException, MessagingException {
     95     if (transport != null && transport.isOpen()) {
     96       return;
     97     }
     98 
     99     try {
    100       // copy configuration into a clean transport, if necessary
    101       if (transport == null) {
    102         transport = imapStore.cloneTransport();
    103       }
    104 
    105       transport.open();
    106 
    107       createParser();
    108 
    109       // The server should greet us with something like
    110       // * OK IMAP4rev1 Server
    111       // consume the response before doing anything else.
    112       ImapResponse response = parser.readResponse(false);
    113       if (!response.isOk()) {
    114         imapStore.getImapHelper().handleEvent(OmtpEvents.DATA_INVALID_INITIAL_SERVER_RESPONSE);
    115         throw new MessagingException(
    116             MessagingException.AUTHENTICATION_FAILED_OR_SERVER_ERROR,
    117             "Invalid server initial response");
    118       }
    119 
    120       queryCapability();
    121 
    122       maybeDoStartTls();
    123 
    124       // LOGIN
    125       doLogin();
    126     } catch (SSLException e) {
    127       LogUtils.d(TAG, "SSLException ", e);
    128       imapStore.getImapHelper().handleEvent(OmtpEvents.DATA_SSL_EXCEPTION);
    129       throw new CertificateValidationException(e.getMessage(), e);
    130     } catch (IOException ioe) {
    131       LogUtils.d(TAG, "IOException", ioe);
    132       imapStore.getImapHelper().handleEvent(OmtpEvents.DATA_IOE_ON_OPEN);
    133       throw ioe;
    134     } finally {
    135       destroyResponses();
    136     }
    137   }
    138 
    139   void logout() {
    140     try {
    141       sendCommand(ImapConstants.LOGOUT, false);
    142       if (!parser.readResponse(true).is(0, ImapConstants.BYE)) {
    143         VvmLog.e(TAG, "Server did not respond LOGOUT with BYE");
    144       }
    145       if (!parser.readResponse(false).isOk()) {
    146         VvmLog.e(TAG, "Server did not respond OK after LOGOUT");
    147       }
    148     } catch (IOException | MessagingException e) {
    149       VvmLog.e(TAG, "Error while logging out:" + e);
    150     }
    151   }
    152 
    153   /**
    154    * Closes the connection and releases all resources. This connection can not be used again until
    155    * {@link #setStore(ImapStore)} is called.
    156    */
    157   void close() {
    158     if (transport != null) {
    159       logout();
    160       transport.close();
    161       transport = null;
    162     }
    163     destroyResponses();
    164     parser = null;
    165     imapStore = null;
    166   }
    167 
    168   /** Attempts to convert the connection into secure connection. */
    169   private void maybeDoStartTls() throws IOException, MessagingException {
    170     // STARTTLS is required in the OMTP standard but not every implementation support it.
    171     // Make sure the server does have this capability
    172     if (hasCapability(ImapConstants.CAPABILITY_STARTTLS)) {
    173       executeSimpleCommand(ImapConstants.STARTTLS);
    174       transport.reopenTls();
    175       createParser();
    176       // The cached capabilities should be refreshed after TLS is established.
    177       queryCapability();
    178     }
    179   }
    180 
    181   /** Logs into the IMAP server */
    182   private void doLogin() throws IOException, MessagingException, AuthenticationFailedException {
    183     try {
    184       if (capabilities.contains(ImapConstants.CAPABILITY_AUTH_DIGEST_MD5)) {
    185         doDigestMd5Auth();
    186       } else {
    187         executeSimpleCommand(getLoginPhrase(), true);
    188       }
    189     } catch (ImapException ie) {
    190       LogUtils.d(TAG, "ImapException", ie);
    191       String status = ie.getStatus();
    192       String statusMessage = ie.getStatusMessage();
    193       String alertText = ie.getAlertText();
    194 
    195       if (ImapConstants.NO.equals(status)) {
    196         switch (statusMessage) {
    197           case ImapConstants.NO_UNKNOWN_USER:
    198             imapStore.getImapHelper().handleEvent(OmtpEvents.DATA_AUTH_UNKNOWN_USER);
    199             break;
    200           case ImapConstants.NO_UNKNOWN_CLIENT:
    201             imapStore.getImapHelper().handleEvent(OmtpEvents.DATA_AUTH_UNKNOWN_DEVICE);
    202             break;
    203           case ImapConstants.NO_INVALID_PASSWORD:
    204             imapStore.getImapHelper().handleEvent(OmtpEvents.DATA_AUTH_INVALID_PASSWORD);
    205             break;
    206           case ImapConstants.NO_MAILBOX_NOT_INITIALIZED:
    207             imapStore.getImapHelper().handleEvent(OmtpEvents.DATA_AUTH_MAILBOX_NOT_INITIALIZED);
    208             break;
    209           case ImapConstants.NO_SERVICE_IS_NOT_PROVISIONED:
    210             imapStore.getImapHelper().handleEvent(OmtpEvents.DATA_AUTH_SERVICE_NOT_PROVISIONED);
    211             break;
    212           case ImapConstants.NO_SERVICE_IS_NOT_ACTIVATED:
    213             imapStore.getImapHelper().handleEvent(OmtpEvents.DATA_AUTH_SERVICE_NOT_ACTIVATED);
    214             break;
    215           case ImapConstants.NO_USER_IS_BLOCKED:
    216             imapStore.getImapHelper().handleEvent(OmtpEvents.DATA_AUTH_USER_IS_BLOCKED);
    217             break;
    218           case ImapConstants.NO_APPLICATION_ERROR:
    219             imapStore.getImapHelper().handleEvent(OmtpEvents.DATA_REJECTED_SERVER_RESPONSE);
    220             break;
    221           default:
    222             imapStore.getImapHelper().handleEvent(OmtpEvents.DATA_BAD_IMAP_CREDENTIAL);
    223         }
    224         throw new AuthenticationFailedException(alertText, ie);
    225       }
    226 
    227       imapStore.getImapHelper().handleEvent(OmtpEvents.DATA_REJECTED_SERVER_RESPONSE);
    228       throw new MessagingException(alertText, ie);
    229     }
    230   }
    231 
    232   private void doDigestMd5Auth() throws IOException, MessagingException {
    233 
    234     //  Initiate the authentication.
    235     //  The server will issue us a challenge, asking to run MD5 on the nonce with our password
    236     //  and other data, including the cnonce we randomly generated.
    237     //
    238     //  C: a AUTHENTICATE DIGEST-MD5
    239     //  S: (BASE64) realm="elwood.innosoft.com",nonce="OA6MG9tEQGm2hh",qop="auth",
    240     //             algorithm=md5-sess,charset=utf-8
    241     List<ImapResponse> responses =
    242         executeSimpleCommand(ImapConstants.AUTHENTICATE + " " + ImapConstants.AUTH_DIGEST_MD5);
    243     String decodedChallenge = decodeBase64(responses.get(0).getStringOrEmpty(0).getString());
    244 
    245     Map<String, String> challenge = DigestMd5Utils.parseDigestMessage(decodedChallenge);
    246     DigestMd5Utils.Data data = new DigestMd5Utils.Data(imapStore, transport, challenge);
    247 
    248     String response = data.createResponse();
    249     //  Respond to the challenge. If the server accepts it, it will reply a response-auth which
    250     //  is the MD5 of our password and the cnonce we've provided, to prove the server does know
    251     //  the password.
    252     //
    253     //  C: (BASE64) charset=utf-8,username="chris",realm="elwood.innosoft.com",
    254     //              nonce="OA6MG9tEQGm2hh",nc=00000001,cnonce="OA6MHXh6VqTrRk",
    255     //              digest-uri="imap/elwood.innosoft.com",
    256     //              response=d388dad90d4bbd760a152321f2143af7,qop=auth
    257     //  S: (BASE64) rspauth=ea40f60335c427b5527b84dbabcdfffd
    258 
    259     responses = executeContinuationResponse(encodeBase64(response), true);
    260 
    261     // Verify response-auth.
    262     // If failed verifyResponseAuth() will throw a MessagingException, terminating the
    263     // connection
    264     String decodedResponseAuth = decodeBase64(responses.get(0).getStringOrEmpty(0).getString());
    265     data.verifyResponseAuth(decodedResponseAuth);
    266 
    267     //  Send a empty response to indicate we've accepted the response-auth
    268     //
    269     //  C: (empty)
    270     //  S: a OK User logged in
    271     executeContinuationResponse("", false);
    272   }
    273 
    274   private static String decodeBase64(String string) {
    275     return new String(Base64.decode(string, Base64.DEFAULT));
    276   }
    277 
    278   private static String encodeBase64(String string) {
    279     return Base64.encodeToString(string.getBytes(), Base64.NO_WRAP);
    280   }
    281 
    282   private void queryCapability() throws IOException, MessagingException {
    283     List<ImapResponse> responses = executeSimpleCommand(ImapConstants.CAPABILITY);
    284     capabilities.clear();
    285     Set<String> disabledCapabilities =
    286         imapStore.getImapHelper().getConfig().getDisabledCapabilities();
    287     for (ImapResponse response : responses) {
    288       if (response.isTagged()) {
    289         continue;
    290       }
    291       for (int i = 0; i < response.size(); i++) {
    292         String capability = response.getStringOrEmpty(i).getString();
    293         if (disabledCapabilities != null) {
    294           if (!disabledCapabilities.contains(capability)) {
    295             capabilities.add(capability);
    296           }
    297         } else {
    298           capabilities.add(capability);
    299         }
    300       }
    301     }
    302 
    303     LogUtils.d(TAG, "Capabilities: " + capabilities.toString());
    304   }
    305 
    306   private boolean hasCapability(String capability) {
    307     return capabilities.contains(capability);
    308   }
    309   /**
    310    * Create an {@link ImapResponseParser} from {@code mTransport.getInputStream()} and set it to
    311    * {@link #parser}.
    312    *
    313    * <p>If we already have an {@link ImapResponseParser}, we {@link #destroyResponses()} and throw
    314    * it away.
    315    */
    316   private void createParser() {
    317     destroyResponses();
    318     parser = new ImapResponseParser(transport.getInputStream());
    319   }
    320 
    321   public void destroyResponses() {
    322     if (parser != null) {
    323       parser.destroyResponses();
    324     }
    325   }
    326 
    327   public ImapResponse readResponse() throws IOException, MessagingException {
    328     return parser.readResponse(false);
    329   }
    330 
    331   public List<ImapResponse> executeSimpleCommand(String command)
    332       throws IOException, MessagingException {
    333     return executeSimpleCommand(command, false);
    334   }
    335 
    336   /**
    337    * Send a single command to the server. The command will be preceded by an IMAP command tag and
    338    * followed by \r\n (caller need not supply them). Execute a simple command at the server, a
    339    * simple command being one that is sent in a single line of text
    340    *
    341    * @param command the command to send to the server
    342    * @param sensitive whether the command should be redacted in logs (used for login)
    343    * @return a list of ImapResponses
    344    * @throws IOException
    345    * @throws MessagingException
    346    */
    347   public List<ImapResponse> executeSimpleCommand(String command, boolean sensitive)
    348       throws IOException, MessagingException {
    349     // TODO: It may be nice to catch IOExceptions and close the connection here.
    350     // Currently, we expect callers to do that, but if they fail to we'll be in a broken state.
    351     sendCommand(command, sensitive);
    352     return getCommandResponses();
    353   }
    354 
    355   public String sendCommand(String command, boolean sensitive)
    356       throws IOException, MessagingException {
    357     open();
    358 
    359     if (transport == null) {
    360       throw new IOException("Null transport");
    361     }
    362     String tag = Integer.toString(nextCommandTag.incrementAndGet());
    363     String commandToSend = tag + " " + command;
    364     transport.writeLine(commandToSend, (sensitive ? IMAP_REDACTED_LOG : command));
    365     return tag;
    366   }
    367 
    368   List<ImapResponse> executeContinuationResponse(String response, boolean sensitive)
    369       throws IOException, MessagingException {
    370     transport.writeLine(response, (sensitive ? IMAP_REDACTED_LOG : response));
    371     return getCommandResponses();
    372   }
    373 
    374   /**
    375    * Read and return all of the responses from the most recent command sent to the server
    376    *
    377    * @return a list of ImapResponses
    378    * @throws IOException
    379    * @throws MessagingException
    380    */
    381   List<ImapResponse> getCommandResponses() throws IOException, MessagingException {
    382     final List<ImapResponse> responses = new ArrayList<ImapResponse>();
    383     ImapResponse response;
    384     do {
    385       response = parser.readResponse(false);
    386       responses.add(response);
    387     } while (!(response.isTagged() || response.isContinuationRequest()));
    388 
    389     if (!(response.isOk() || response.isContinuationRequest())) {
    390       final String toString = response.toString();
    391       final String status = response.getStatusOrEmpty().getString();
    392       final String statusMessage = response.getStatusResponseTextOrEmpty().getString();
    393       final String alert = response.getAlertTextOrEmpty().getString();
    394       final String responseCode = response.getResponseCodeOrEmpty().getString();
    395       destroyResponses();
    396       throw new ImapException(toString, status, statusMessage, alert, responseCode);
    397     }
    398     return responses;
    399   }
    400 }
    401