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