Home | History | Annotate | Download | only in imap
      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.imap;
     17 
     18 import android.content.Context;
     19 import android.net.ConnectivityManager;
     20 import android.net.Network;
     21 import android.net.NetworkInfo;
     22 import android.support.annotation.Nullable;
     23 import android.telecom.PhoneAccountHandle;
     24 import android.util.Base64;
     25 import com.android.voicemail.impl.OmtpConstants;
     26 import com.android.voicemail.impl.OmtpConstants.ChangePinResult;
     27 import com.android.voicemail.impl.OmtpEvents;
     28 import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
     29 import com.android.voicemail.impl.VisualVoicemailPreferences;
     30 import com.android.voicemail.impl.Voicemail;
     31 import com.android.voicemail.impl.VoicemailStatus;
     32 import com.android.voicemail.impl.VoicemailStatus.Editor;
     33 import com.android.voicemail.impl.VvmLog;
     34 import com.android.voicemail.impl.fetch.VoicemailFetchedCallback;
     35 import com.android.voicemail.impl.mail.Address;
     36 import com.android.voicemail.impl.mail.Body;
     37 import com.android.voicemail.impl.mail.BodyPart;
     38 import com.android.voicemail.impl.mail.FetchProfile;
     39 import com.android.voicemail.impl.mail.Flag;
     40 import com.android.voicemail.impl.mail.Message;
     41 import com.android.voicemail.impl.mail.MessagingException;
     42 import com.android.voicemail.impl.mail.Multipart;
     43 import com.android.voicemail.impl.mail.TempDirectory;
     44 import com.android.voicemail.impl.mail.internet.MimeMessage;
     45 import com.android.voicemail.impl.mail.store.ImapConnection;
     46 import com.android.voicemail.impl.mail.store.ImapFolder;
     47 import com.android.voicemail.impl.mail.store.ImapFolder.Quota;
     48 import com.android.voicemail.impl.mail.store.ImapStore;
     49 import com.android.voicemail.impl.mail.store.imap.ImapConstants;
     50 import com.android.voicemail.impl.mail.store.imap.ImapResponse;
     51 import com.android.voicemail.impl.mail.utils.LogUtils;
     52 import com.android.voicemail.impl.sync.OmtpVvmSyncService.TranscriptionFetchedCallback;
     53 import java.io.BufferedOutputStream;
     54 import java.io.ByteArrayOutputStream;
     55 import java.io.Closeable;
     56 import java.io.IOException;
     57 import java.util.ArrayList;
     58 import java.util.Arrays;
     59 import java.util.List;
     60 import java.util.Locale;
     61 import org.apache.commons.io.IOUtils;
     62 
     63 /** A helper interface to abstract commands sent across IMAP interface for a given account. */
     64 public class ImapHelper implements Closeable {
     65 
     66   private static final String TAG = "ImapHelper";
     67 
     68   private ImapFolder mFolder;
     69   private ImapStore mImapStore;
     70 
     71   private final Context mContext;
     72   private final PhoneAccountHandle mPhoneAccount;
     73   private final Network mNetwork;
     74   private final Editor mStatus;
     75 
     76   VisualVoicemailPreferences mPrefs;
     77 
     78   private final OmtpVvmCarrierConfigHelper mConfig;
     79 
     80   /** InitializingException */
     81   public static class InitializingException extends Exception {
     82 
     83     public InitializingException(String message) {
     84       super(message);
     85     }
     86   }
     87 
     88   public ImapHelper(
     89       Context context, PhoneAccountHandle phoneAccount, Network network, Editor status)
     90       throws InitializingException {
     91     this(
     92         context,
     93         new OmtpVvmCarrierConfigHelper(context, phoneAccount),
     94         phoneAccount,
     95         network,
     96         status);
     97   }
     98 
     99   public ImapHelper(
    100       Context context,
    101       OmtpVvmCarrierConfigHelper config,
    102       PhoneAccountHandle phoneAccount,
    103       Network network,
    104       Editor status)
    105       throws InitializingException {
    106     mContext = context;
    107     mPhoneAccount = phoneAccount;
    108     mNetwork = network;
    109     mStatus = status;
    110     mConfig = config;
    111     mPrefs = new VisualVoicemailPreferences(context, phoneAccount);
    112 
    113     try {
    114       TempDirectory.setTempDirectory(context);
    115 
    116       String username = mPrefs.getString(OmtpConstants.IMAP_USER_NAME, null);
    117       String password = mPrefs.getString(OmtpConstants.IMAP_PASSWORD, null);
    118       String serverName = mPrefs.getString(OmtpConstants.SERVER_ADDRESS, null);
    119       int port = Integer.parseInt(mPrefs.getString(OmtpConstants.IMAP_PORT, null));
    120       int auth = ImapStore.FLAG_NONE;
    121 
    122       int sslPort = mConfig.getSslPort();
    123       if (sslPort != 0) {
    124         port = sslPort;
    125         auth = ImapStore.FLAG_SSL;
    126       }
    127 
    128       mImapStore =
    129           new ImapStore(context, this, username, password, port, serverName, auth, network);
    130     } catch (NumberFormatException e) {
    131       handleEvent(OmtpEvents.DATA_INVALID_PORT);
    132       LogUtils.w(TAG, "Could not parse port number");
    133       throw new InitializingException("cannot initialize ImapHelper:" + e.toString());
    134     }
    135   }
    136 
    137   @Override
    138   public void close() {
    139     mImapStore.closeConnection();
    140   }
    141 
    142   public boolean isRoaming() {
    143     ConnectivityManager connectivityManager =
    144         (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
    145     NetworkInfo info = connectivityManager.getNetworkInfo(mNetwork);
    146     if (info == null) {
    147       return false;
    148     }
    149     return info.isRoaming();
    150   }
    151 
    152   public OmtpVvmCarrierConfigHelper getConfig() {
    153     return mConfig;
    154   }
    155 
    156   public ImapConnection connect() {
    157     return mImapStore.getConnection();
    158   }
    159 
    160   /** The caller thread will block until the method returns. */
    161   public boolean markMessagesAsRead(List<Voicemail> voicemails) {
    162     return setFlags(voicemails, Flag.SEEN);
    163   }
    164 
    165   /** The caller thread will block until the method returns. */
    166   public boolean markMessagesAsDeleted(List<Voicemail> voicemails) {
    167     return setFlags(voicemails, Flag.DELETED);
    168   }
    169 
    170   public void handleEvent(OmtpEvents event) {
    171     mConfig.handleEvent(mStatus, event);
    172   }
    173 
    174   /**
    175    * Set flags on the server for a given set of voicemails.
    176    *
    177    * @param voicemails The voicemails to set flags for.
    178    * @param flags The flags to set on the voicemails.
    179    * @return {@code true} if the operation completes successfully, {@code false} otherwise.
    180    */
    181   private boolean setFlags(List<Voicemail> voicemails, String... flags) {
    182     if (voicemails.size() == 0) {
    183       return false;
    184     }
    185     try {
    186       mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
    187       if (mFolder != null) {
    188         mFolder.setFlags(convertToImapMessages(voicemails), flags, true);
    189         return true;
    190       }
    191       return false;
    192     } catch (MessagingException e) {
    193       LogUtils.e(TAG, e, "Messaging exception");
    194       return false;
    195     } finally {
    196       closeImapFolder();
    197     }
    198   }
    199 
    200   /**
    201    * Fetch a list of voicemails from the server.
    202    *
    203    * @return A list of voicemail objects containing data about voicemails stored on the server.
    204    */
    205   public List<Voicemail> fetchAllVoicemails() {
    206     List<Voicemail> result = new ArrayList<Voicemail>();
    207     Message[] messages;
    208     try {
    209       mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
    210       if (mFolder == null) {
    211         // This means we were unable to successfully open the folder.
    212         return null;
    213       }
    214 
    215       // This method retrieves lightweight messages containing only the uid of the message.
    216       messages = mFolder.getMessages(null);
    217 
    218       for (Message message : messages) {
    219         // Get the voicemail details (message structure).
    220         MessageStructureWrapper messageStructureWrapper = fetchMessageStructure(message);
    221         if (messageStructureWrapper != null) {
    222           result.add(getVoicemailFromMessageStructure(messageStructureWrapper));
    223         }
    224       }
    225       return result;
    226     } catch (MessagingException e) {
    227       LogUtils.e(TAG, e, "Messaging Exception");
    228       return null;
    229     } finally {
    230       closeImapFolder();
    231     }
    232   }
    233 
    234   /**
    235    * Extract voicemail details from the message structure. Also fetch transcription if a
    236    * transcription exists.
    237    */
    238   private Voicemail getVoicemailFromMessageStructure(
    239       MessageStructureWrapper messageStructureWrapper) throws MessagingException {
    240     Message messageDetails = messageStructureWrapper.messageStructure;
    241 
    242     TranscriptionFetchedListener listener = new TranscriptionFetchedListener();
    243     if (messageStructureWrapper.transcriptionBodyPart != null) {
    244       FetchProfile fetchProfile = new FetchProfile();
    245       fetchProfile.add(messageStructureWrapper.transcriptionBodyPart);
    246 
    247       mFolder.fetch(new Message[] {messageDetails}, fetchProfile, listener);
    248     }
    249 
    250     // Found an audio attachment, this is a valid voicemail.
    251     long time = messageDetails.getSentDate().getTime();
    252     String number = getNumber(messageDetails.getFrom());
    253     boolean isRead = Arrays.asList(messageDetails.getFlags()).contains(Flag.SEEN);
    254     Long duration = messageDetails.getDuration();
    255     Voicemail.Builder builder =
    256         Voicemail.createForInsertion(time, number)
    257             .setPhoneAccount(mPhoneAccount)
    258             .setSourcePackage(mContext.getPackageName())
    259             .setSourceData(messageDetails.getUid())
    260             .setIsRead(isRead)
    261             .setTranscription(listener.getVoicemailTranscription());
    262     if (duration != null) {
    263       builder.setDuration(duration);
    264     }
    265     return builder.build();
    266   }
    267 
    268   /**
    269    * The "from" field of a visual voicemail IMAP message is the number of the caller who left the
    270    * message. Extract this number from the list of "from" addresses.
    271    *
    272    * @param fromAddresses A list of addresses that comprise the "from" line.
    273    * @return The number of the voicemail sender.
    274    */
    275   private String getNumber(Address[] fromAddresses) {
    276     if (fromAddresses != null && fromAddresses.length > 0) {
    277       if (fromAddresses.length != 1) {
    278         LogUtils.w(TAG, "More than one from addresses found. Using the first one.");
    279       }
    280       String sender = fromAddresses[0].getAddress();
    281       int atPos = sender.indexOf('@');
    282       if (atPos != -1) {
    283         // Strip domain part of the address.
    284         sender = sender.substring(0, atPos);
    285       }
    286       return sender;
    287     }
    288     return null;
    289   }
    290 
    291   /**
    292    * Fetches the structure of the given message and returns a wrapper containing the message
    293    * structure and the transcription structure (if applicable).
    294    *
    295    * @throws MessagingException if fetching the structure of the message fails
    296    */
    297   private MessageStructureWrapper fetchMessageStructure(Message message) throws MessagingException {
    298     LogUtils.d(TAG, "Fetching message structure for " + message.getUid());
    299 
    300     MessageStructureFetchedListener listener = new MessageStructureFetchedListener();
    301 
    302     FetchProfile fetchProfile = new FetchProfile();
    303     fetchProfile.addAll(
    304         Arrays.asList(
    305             FetchProfile.Item.FLAGS, FetchProfile.Item.ENVELOPE, FetchProfile.Item.STRUCTURE));
    306 
    307     // The IMAP folder fetch method will call "messageRetrieved" on the listener when the
    308     // message is successfully retrieved.
    309     mFolder.fetch(new Message[] {message}, fetchProfile, listener);
    310     return listener.getMessageStructure();
    311   }
    312 
    313   public boolean fetchVoicemailPayload(VoicemailFetchedCallback callback, final String uid) {
    314     try {
    315       mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
    316       if (mFolder == null) {
    317         // This means we were unable to successfully open the folder.
    318         return false;
    319       }
    320       Message message = mFolder.getMessage(uid);
    321       if (message == null) {
    322         return false;
    323       }
    324       VoicemailPayload voicemailPayload = fetchVoicemailPayload(message);
    325       callback.setVoicemailContent(voicemailPayload);
    326       return true;
    327     } catch (MessagingException e) {
    328     } finally {
    329       closeImapFolder();
    330     }
    331     return false;
    332   }
    333 
    334   /**
    335    * Fetches the body of the given message and returns the parsed voicemail payload.
    336    *
    337    * @throws MessagingException if fetching the body of the message fails
    338    */
    339   private VoicemailPayload fetchVoicemailPayload(Message message) throws MessagingException {
    340     LogUtils.d(TAG, "Fetching message body for " + message.getUid());
    341 
    342     MessageBodyFetchedListener listener = new MessageBodyFetchedListener();
    343 
    344     FetchProfile fetchProfile = new FetchProfile();
    345     fetchProfile.add(FetchProfile.Item.BODY);
    346 
    347     mFolder.fetch(new Message[] {message}, fetchProfile, listener);
    348     return listener.getVoicemailPayload();
    349   }
    350 
    351   public boolean fetchTranscription(TranscriptionFetchedCallback callback, String uid) {
    352     try {
    353       mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
    354       if (mFolder == null) {
    355         // This means we were unable to successfully open the folder.
    356         return false;
    357       }
    358 
    359       Message message = mFolder.getMessage(uid);
    360       if (message == null) {
    361         return false;
    362       }
    363 
    364       MessageStructureWrapper messageStructureWrapper = fetchMessageStructure(message);
    365       if (messageStructureWrapper != null) {
    366         TranscriptionFetchedListener listener = new TranscriptionFetchedListener();
    367         if (messageStructureWrapper.transcriptionBodyPart != null) {
    368           FetchProfile fetchProfile = new FetchProfile();
    369           fetchProfile.add(messageStructureWrapper.transcriptionBodyPart);
    370 
    371           // This method is called synchronously so the transcription will be populated
    372           // in the listener once the next method is called.
    373           mFolder.fetch(new Message[] {message}, fetchProfile, listener);
    374           callback.setVoicemailTranscription(listener.getVoicemailTranscription());
    375         }
    376       }
    377       return true;
    378     } catch (MessagingException e) {
    379       LogUtils.e(TAG, e, "Messaging Exception");
    380       return false;
    381     } finally {
    382       closeImapFolder();
    383     }
    384   }
    385 
    386   @ChangePinResult
    387   public int changePin(String oldPin, String newPin) throws MessagingException {
    388     ImapConnection connection = mImapStore.getConnection();
    389     try {
    390       String command =
    391           getConfig().getProtocol().getCommand(OmtpConstants.IMAP_CHANGE_TUI_PWD_FORMAT);
    392       connection.sendCommand(String.format(Locale.US, command, newPin, oldPin), true);
    393       return getChangePinResultFromImapResponse(connection.readResponse());
    394     } catch (IOException ioe) {
    395       VvmLog.e(TAG, "changePin: ", ioe);
    396       return OmtpConstants.CHANGE_PIN_SYSTEM_ERROR;
    397     } finally {
    398       connection.destroyResponses();
    399     }
    400   }
    401 
    402   public void changeVoicemailTuiLanguage(String languageCode) throws MessagingException {
    403     ImapConnection connection = mImapStore.getConnection();
    404     try {
    405       String command =
    406           getConfig().getProtocol().getCommand(OmtpConstants.IMAP_CHANGE_VM_LANG_FORMAT);
    407       connection.sendCommand(String.format(Locale.US, command, languageCode), true);
    408     } catch (IOException ioe) {
    409       LogUtils.e(TAG, ioe.toString());
    410     } finally {
    411       connection.destroyResponses();
    412     }
    413   }
    414 
    415   public void closeNewUserTutorial() throws MessagingException {
    416     ImapConnection connection = mImapStore.getConnection();
    417     try {
    418       String command = getConfig().getProtocol().getCommand(OmtpConstants.IMAP_CLOSE_NUT);
    419       connection.executeSimpleCommand(command, false);
    420     } catch (IOException ioe) {
    421       throw new MessagingException(MessagingException.SERVER_ERROR, ioe.toString());
    422     } finally {
    423       connection.destroyResponses();
    424     }
    425   }
    426 
    427   @ChangePinResult
    428   private static int getChangePinResultFromImapResponse(ImapResponse response)
    429       throws MessagingException {
    430     if (!response.isTagged()) {
    431       throw new MessagingException(MessagingException.SERVER_ERROR, "tagged response expected");
    432     }
    433     if (!response.isOk()) {
    434       String message = response.getStringOrEmpty(1).getString();
    435       LogUtils.d(TAG, "change PIN failed: " + message);
    436       if (OmtpConstants.RESPONSE_CHANGE_PIN_TOO_SHORT.equals(message)) {
    437         return OmtpConstants.CHANGE_PIN_TOO_SHORT;
    438       }
    439       if (OmtpConstants.RESPONSE_CHANGE_PIN_TOO_LONG.equals(message)) {
    440         return OmtpConstants.CHANGE_PIN_TOO_LONG;
    441       }
    442       if (OmtpConstants.RESPONSE_CHANGE_PIN_TOO_WEAK.equals(message)) {
    443         return OmtpConstants.CHANGE_PIN_TOO_WEAK;
    444       }
    445       if (OmtpConstants.RESPONSE_CHANGE_PIN_MISMATCH.equals(message)) {
    446         return OmtpConstants.CHANGE_PIN_MISMATCH;
    447       }
    448       if (OmtpConstants.RESPONSE_CHANGE_PIN_INVALID_CHARACTER.equals(message)) {
    449         return OmtpConstants.CHANGE_PIN_INVALID_CHARACTER;
    450       }
    451       return OmtpConstants.CHANGE_PIN_SYSTEM_ERROR;
    452     }
    453     LogUtils.d(TAG, "change PIN succeeded");
    454     return OmtpConstants.CHANGE_PIN_SUCCESS;
    455   }
    456 
    457   public void updateQuota() {
    458     try {
    459       mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
    460       if (mFolder == null) {
    461         // This means we were unable to successfully open the folder.
    462         return;
    463       }
    464       updateQuota(mFolder);
    465     } catch (MessagingException e) {
    466       LogUtils.e(TAG, e, "Messaging Exception");
    467     } finally {
    468       closeImapFolder();
    469     }
    470   }
    471 
    472   @Nullable
    473   public Quota getQuota() {
    474     try {
    475       mFolder = openImapFolder(ImapFolder.MODE_READ_ONLY);
    476       if (mFolder == null) {
    477         // This means we were unable to successfully open the folder.
    478         LogUtils.e(TAG, "Unable to open folder");
    479         return null;
    480       }
    481       return mFolder.getQuota();
    482     } catch (MessagingException e) {
    483       LogUtils.e(TAG, e, "Messaging Exception");
    484       return null;
    485     } finally {
    486       closeImapFolder();
    487     }
    488   }
    489 
    490   private void updateQuota(ImapFolder folder) throws MessagingException {
    491     setQuota(folder.getQuota());
    492   }
    493 
    494   private void setQuota(ImapFolder.Quota quota) {
    495     if (quota == null) {
    496       LogUtils.i(TAG, "quota was null");
    497       return;
    498     }
    499 
    500     LogUtils.i(
    501         TAG,
    502         "Updating Voicemail status table with"
    503             + " quota occupied: "
    504             + quota.occupied
    505             + " new quota total:"
    506             + quota.total);
    507     VoicemailStatus.edit(mContext, mPhoneAccount).setQuota(quota.occupied, quota.total).apply();
    508     LogUtils.i(TAG, "Updated quota occupied and total");
    509   }
    510 
    511   /**
    512    * A wrapper to hold a message with its header details and the structure for transcriptions (so
    513    * they can be fetched in the future).
    514    */
    515   public static class MessageStructureWrapper {
    516 
    517     public Message messageStructure;
    518     public BodyPart transcriptionBodyPart;
    519 
    520     public MessageStructureWrapper() {}
    521   }
    522 
    523   /** Listener for the message structure being fetched. */
    524   private final class MessageStructureFetchedListener
    525       implements ImapFolder.MessageRetrievalListener {
    526 
    527     private MessageStructureWrapper mMessageStructure;
    528 
    529     public MessageStructureFetchedListener() {}
    530 
    531     public MessageStructureWrapper getMessageStructure() {
    532       return mMessageStructure;
    533     }
    534 
    535     @Override
    536     public void messageRetrieved(Message message) {
    537       LogUtils.d(TAG, "Fetched message structure for " + message.getUid());
    538       LogUtils.d(TAG, "Message retrieved: " + message);
    539       try {
    540         mMessageStructure = getMessageOrNull(message);
    541         if (mMessageStructure == null) {
    542           LogUtils.d(TAG, "This voicemail does not have an attachment...");
    543           return;
    544         }
    545       } catch (MessagingException e) {
    546         LogUtils.e(TAG, e, "Messaging Exception");
    547         closeImapFolder();
    548       }
    549     }
    550 
    551     /**
    552      * Check if this IMAP message is a valid voicemail and whether it contains a transcription.
    553      *
    554      * @param message The IMAP message.
    555      * @return The MessageStructureWrapper object corresponding to an IMAP message and
    556      *     transcription.
    557      */
    558     private MessageStructureWrapper getMessageOrNull(Message message) throws MessagingException {
    559       if (!message.getMimeType().startsWith("multipart/")) {
    560         LogUtils.w(TAG, "Ignored non multi-part message");
    561         return null;
    562       }
    563 
    564       MessageStructureWrapper messageStructureWrapper = new MessageStructureWrapper();
    565 
    566       Multipart multipart = (Multipart) message.getBody();
    567       for (int i = 0; i < multipart.getCount(); ++i) {
    568         BodyPart bodyPart = multipart.getBodyPart(i);
    569         String bodyPartMimeType = bodyPart.getMimeType().toLowerCase();
    570         LogUtils.d(TAG, "bodyPart mime type: " + bodyPartMimeType);
    571 
    572         if (bodyPartMimeType.startsWith("audio/")) {
    573           messageStructureWrapper.messageStructure = message;
    574         } else if (bodyPartMimeType.startsWith("text/")) {
    575           messageStructureWrapper.transcriptionBodyPart = bodyPart;
    576         } else {
    577           VvmLog.v(TAG, "Unknown bodyPart MIME: " + bodyPartMimeType);
    578         }
    579       }
    580 
    581       if (messageStructureWrapper.messageStructure != null) {
    582         return messageStructureWrapper;
    583       }
    584 
    585       // No attachment found, this is not a voicemail.
    586       return null;
    587     }
    588   }
    589 
    590   /** Listener for the message body being fetched. */
    591   private final class MessageBodyFetchedListener implements ImapFolder.MessageRetrievalListener {
    592 
    593     private VoicemailPayload mVoicemailPayload;
    594 
    595     /** Returns the fetch voicemail payload. */
    596     public VoicemailPayload getVoicemailPayload() {
    597       return mVoicemailPayload;
    598     }
    599 
    600     @Override
    601     public void messageRetrieved(Message message) {
    602       LogUtils.d(TAG, "Fetched message body for " + message.getUid());
    603       LogUtils.d(TAG, "Message retrieved: " + message);
    604       try {
    605         mVoicemailPayload = getVoicemailPayloadFromMessage(message);
    606       } catch (MessagingException e) {
    607         LogUtils.e(TAG, "Messaging Exception:", e);
    608       } catch (IOException e) {
    609         LogUtils.e(TAG, "IO Exception:", e);
    610       }
    611     }
    612 
    613     private VoicemailPayload getVoicemailPayloadFromMessage(Message message)
    614         throws MessagingException, IOException {
    615       Multipart multipart = (Multipart) message.getBody();
    616       List<String> mimeTypes = new ArrayList<>();
    617       for (int i = 0; i < multipart.getCount(); ++i) {
    618         BodyPart bodyPart = multipart.getBodyPart(i);
    619         String bodyPartMimeType = bodyPart.getMimeType().toLowerCase();
    620         mimeTypes.add(bodyPartMimeType);
    621         if (bodyPartMimeType.startsWith("audio/")) {
    622           byte[] bytes = getDataFromBody(bodyPart.getBody());
    623           LogUtils.d(TAG, String.format("Fetched %s bytes of data", bytes.length));
    624           return new VoicemailPayload(bodyPartMimeType, bytes);
    625         }
    626       }
    627       LogUtils.e(TAG, "No audio attachment found on this voicemail, mimeTypes:" + mimeTypes);
    628       return null;
    629     }
    630   }
    631 
    632   /** Listener for the transcription being fetched. */
    633   private final class TranscriptionFetchedListener implements ImapFolder.MessageRetrievalListener {
    634 
    635     private String mVoicemailTranscription;
    636 
    637     /** Returns the fetched voicemail transcription. */
    638     public String getVoicemailTranscription() {
    639       return mVoicemailTranscription;
    640     }
    641 
    642     @Override
    643     public void messageRetrieved(Message message) {
    644       LogUtils.d(TAG, "Fetched transcription for " + message.getUid());
    645       try {
    646         mVoicemailTranscription = new String(getDataFromBody(message.getBody()));
    647       } catch (MessagingException e) {
    648         LogUtils.e(TAG, "Messaging Exception:", e);
    649       } catch (IOException e) {
    650         LogUtils.e(TAG, "IO Exception:", e);
    651       }
    652     }
    653   }
    654 
    655   private ImapFolder openImapFolder(String modeReadWrite) {
    656     try {
    657       if (mImapStore == null) {
    658         return null;
    659       }
    660       ImapFolder folder = new ImapFolder(mImapStore, ImapConstants.INBOX);
    661       folder.open(modeReadWrite);
    662       return folder;
    663     } catch (MessagingException e) {
    664       LogUtils.e(TAG, e, "Messaging Exception");
    665     }
    666     return null;
    667   }
    668 
    669   private Message[] convertToImapMessages(List<Voicemail> voicemails) {
    670     Message[] messages = new Message[voicemails.size()];
    671     for (int i = 0; i < voicemails.size(); ++i) {
    672       messages[i] = new MimeMessage();
    673       messages[i].setUid(voicemails.get(i).getSourceData());
    674     }
    675     return messages;
    676   }
    677 
    678   private void closeImapFolder() {
    679     if (mFolder != null) {
    680       mFolder.close(true);
    681     }
    682   }
    683 
    684   private byte[] getDataFromBody(Body body) throws IOException, MessagingException {
    685     ByteArrayOutputStream out = new ByteArrayOutputStream();
    686     BufferedOutputStream bufferedOut = new BufferedOutputStream(out);
    687     try {
    688       body.writeTo(bufferedOut);
    689       return Base64.decode(out.toByteArray(), Base64.DEFAULT);
    690     } finally {
    691       IOUtils.closeQuietly(bufferedOut);
    692       IOUtils.closeQuietly(out);
    693     }
    694   }
    695 }
    696