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     return Voicemail.createForInsertion(time, number)
    255         .setPhoneAccount(mPhoneAccount)
    256         .setSourcePackage(mContext.getPackageName())
    257         .setSourceData(messageDetails.getUid())
    258         .setIsRead(isRead)
    259         .setTranscription(listener.getVoicemailTranscription())
    260         .build();
    261   }
    262 
    263   /**
    264    * The "from" field of a visual voicemail IMAP message is the number of the caller who left the
    265    * message. Extract this number from the list of "from" addresses.
    266    *
    267    * @param fromAddresses A list of addresses that comprise the "from" line.
    268    * @return The number of the voicemail sender.
    269    */
    270   private String getNumber(Address[] fromAddresses) {
    271     if (fromAddresses != null && fromAddresses.length > 0) {
    272       if (fromAddresses.length != 1) {
    273         LogUtils.w(TAG, "More than one from addresses found. Using the first one.");
    274       }
    275       String sender = fromAddresses[0].getAddress();
    276       int atPos = sender.indexOf('@');
    277       if (atPos != -1) {
    278         // Strip domain part of the address.
    279         sender = sender.substring(0, atPos);
    280       }
    281       return sender;
    282     }
    283     return null;
    284   }
    285 
    286   /**
    287    * Fetches the structure of the given message and returns a wrapper containing the message
    288    * structure and the transcription structure (if applicable).
    289    *
    290    * @throws MessagingException if fetching the structure of the message fails
    291    */
    292   private MessageStructureWrapper fetchMessageStructure(Message message) throws MessagingException {
    293     LogUtils.d(TAG, "Fetching message structure for " + message.getUid());
    294 
    295     MessageStructureFetchedListener listener = new MessageStructureFetchedListener();
    296 
    297     FetchProfile fetchProfile = new FetchProfile();
    298     fetchProfile.addAll(
    299         Arrays.asList(
    300             FetchProfile.Item.FLAGS, FetchProfile.Item.ENVELOPE, FetchProfile.Item.STRUCTURE));
    301 
    302     // The IMAP folder fetch method will call "messageRetrieved" on the listener when the
    303     // message is successfully retrieved.
    304     mFolder.fetch(new Message[] {message}, fetchProfile, listener);
    305     return listener.getMessageStructure();
    306   }
    307 
    308   public boolean fetchVoicemailPayload(VoicemailFetchedCallback callback, final String uid) {
    309     try {
    310       mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
    311       if (mFolder == null) {
    312         // This means we were unable to successfully open the folder.
    313         return false;
    314       }
    315       Message message = mFolder.getMessage(uid);
    316       if (message == null) {
    317         return false;
    318       }
    319       VoicemailPayload voicemailPayload = fetchVoicemailPayload(message);
    320       callback.setVoicemailContent(voicemailPayload);
    321       return true;
    322     } catch (MessagingException e) {
    323     } finally {
    324       closeImapFolder();
    325     }
    326     return false;
    327   }
    328 
    329   /**
    330    * Fetches the body of the given message and returns the parsed voicemail payload.
    331    *
    332    * @throws MessagingException if fetching the body of the message fails
    333    */
    334   private VoicemailPayload fetchVoicemailPayload(Message message) throws MessagingException {
    335     LogUtils.d(TAG, "Fetching message body for " + message.getUid());
    336 
    337     MessageBodyFetchedListener listener = new MessageBodyFetchedListener();
    338 
    339     FetchProfile fetchProfile = new FetchProfile();
    340     fetchProfile.add(FetchProfile.Item.BODY);
    341 
    342     mFolder.fetch(new Message[] {message}, fetchProfile, listener);
    343     return listener.getVoicemailPayload();
    344   }
    345 
    346   public boolean fetchTranscription(TranscriptionFetchedCallback callback, String uid) {
    347     try {
    348       mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
    349       if (mFolder == null) {
    350         // This means we were unable to successfully open the folder.
    351         return false;
    352       }
    353 
    354       Message message = mFolder.getMessage(uid);
    355       if (message == null) {
    356         return false;
    357       }
    358 
    359       MessageStructureWrapper messageStructureWrapper = fetchMessageStructure(message);
    360       if (messageStructureWrapper != null) {
    361         TranscriptionFetchedListener listener = new TranscriptionFetchedListener();
    362         if (messageStructureWrapper.transcriptionBodyPart != null) {
    363           FetchProfile fetchProfile = new FetchProfile();
    364           fetchProfile.add(messageStructureWrapper.transcriptionBodyPart);
    365 
    366           // This method is called synchronously so the transcription will be populated
    367           // in the listener once the next method is called.
    368           mFolder.fetch(new Message[] {message}, fetchProfile, listener);
    369           callback.setVoicemailTranscription(listener.getVoicemailTranscription());
    370         }
    371       }
    372       return true;
    373     } catch (MessagingException e) {
    374       LogUtils.e(TAG, e, "Messaging Exception");
    375       return false;
    376     } finally {
    377       closeImapFolder();
    378     }
    379   }
    380 
    381   @ChangePinResult
    382   public int changePin(String oldPin, String newPin) throws MessagingException {
    383     ImapConnection connection = mImapStore.getConnection();
    384     try {
    385       String command =
    386           getConfig().getProtocol().getCommand(OmtpConstants.IMAP_CHANGE_TUI_PWD_FORMAT);
    387       connection.sendCommand(String.format(Locale.US, command, newPin, oldPin), true);
    388       return getChangePinResultFromImapResponse(connection.readResponse());
    389     } catch (IOException ioe) {
    390       VvmLog.e(TAG, "changePin: ", ioe);
    391       return OmtpConstants.CHANGE_PIN_SYSTEM_ERROR;
    392     } finally {
    393       connection.destroyResponses();
    394     }
    395   }
    396 
    397   public void changeVoicemailTuiLanguage(String languageCode) throws MessagingException {
    398     ImapConnection connection = mImapStore.getConnection();
    399     try {
    400       String command =
    401           getConfig().getProtocol().getCommand(OmtpConstants.IMAP_CHANGE_VM_LANG_FORMAT);
    402       connection.sendCommand(String.format(Locale.US, command, languageCode), true);
    403     } catch (IOException ioe) {
    404       LogUtils.e(TAG, ioe.toString());
    405     } finally {
    406       connection.destroyResponses();
    407     }
    408   }
    409 
    410   public void closeNewUserTutorial() throws MessagingException {
    411     ImapConnection connection = mImapStore.getConnection();
    412     try {
    413       String command = getConfig().getProtocol().getCommand(OmtpConstants.IMAP_CLOSE_NUT);
    414       connection.executeSimpleCommand(command, false);
    415     } catch (IOException ioe) {
    416       throw new MessagingException(MessagingException.SERVER_ERROR, ioe.toString());
    417     } finally {
    418       connection.destroyResponses();
    419     }
    420   }
    421 
    422   @ChangePinResult
    423   private static int getChangePinResultFromImapResponse(ImapResponse response)
    424       throws MessagingException {
    425     if (!response.isTagged()) {
    426       throw new MessagingException(MessagingException.SERVER_ERROR, "tagged response expected");
    427     }
    428     if (!response.isOk()) {
    429       String message = response.getStringOrEmpty(1).getString();
    430       LogUtils.d(TAG, "change PIN failed: " + message);
    431       if (OmtpConstants.RESPONSE_CHANGE_PIN_TOO_SHORT.equals(message)) {
    432         return OmtpConstants.CHANGE_PIN_TOO_SHORT;
    433       }
    434       if (OmtpConstants.RESPONSE_CHANGE_PIN_TOO_LONG.equals(message)) {
    435         return OmtpConstants.CHANGE_PIN_TOO_LONG;
    436       }
    437       if (OmtpConstants.RESPONSE_CHANGE_PIN_TOO_WEAK.equals(message)) {
    438         return OmtpConstants.CHANGE_PIN_TOO_WEAK;
    439       }
    440       if (OmtpConstants.RESPONSE_CHANGE_PIN_MISMATCH.equals(message)) {
    441         return OmtpConstants.CHANGE_PIN_MISMATCH;
    442       }
    443       if (OmtpConstants.RESPONSE_CHANGE_PIN_INVALID_CHARACTER.equals(message)) {
    444         return OmtpConstants.CHANGE_PIN_INVALID_CHARACTER;
    445       }
    446       return OmtpConstants.CHANGE_PIN_SYSTEM_ERROR;
    447     }
    448     LogUtils.d(TAG, "change PIN succeeded");
    449     return OmtpConstants.CHANGE_PIN_SUCCESS;
    450   }
    451 
    452   public void updateQuota() {
    453     try {
    454       mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
    455       if (mFolder == null) {
    456         // This means we were unable to successfully open the folder.
    457         return;
    458       }
    459       updateQuota(mFolder);
    460     } catch (MessagingException e) {
    461       LogUtils.e(TAG, e, "Messaging Exception");
    462     } finally {
    463       closeImapFolder();
    464     }
    465   }
    466 
    467   @Nullable
    468   public Quota getQuota() {
    469     try {
    470       mFolder = openImapFolder(ImapFolder.MODE_READ_ONLY);
    471       if (mFolder == null) {
    472         // This means we were unable to successfully open the folder.
    473         LogUtils.e(TAG, "Unable to open folder");
    474         return null;
    475       }
    476       return mFolder.getQuota();
    477     } catch (MessagingException e) {
    478       LogUtils.e(TAG, e, "Messaging Exception");
    479       return null;
    480     } finally {
    481       closeImapFolder();
    482     }
    483   }
    484 
    485   private void updateQuota(ImapFolder folder) throws MessagingException {
    486     setQuota(folder.getQuota());
    487   }
    488 
    489   private void setQuota(ImapFolder.Quota quota) {
    490     if (quota == null) {
    491       LogUtils.i(TAG, "quota was null");
    492       return;
    493     }
    494 
    495     LogUtils.i(
    496         TAG,
    497         "Updating Voicemail status table with"
    498             + " quota occupied: "
    499             + quota.occupied
    500             + " new quota total:"
    501             + quota.total);
    502     VoicemailStatus.edit(mContext, mPhoneAccount).setQuota(quota.occupied, quota.total).apply();
    503     LogUtils.i(TAG, "Updated quota occupied and total");
    504   }
    505 
    506   /**
    507    * A wrapper to hold a message with its header details and the structure for transcriptions (so
    508    * they can be fetched in the future).
    509    */
    510   public static class MessageStructureWrapper {
    511 
    512     public Message messageStructure;
    513     public BodyPart transcriptionBodyPart;
    514 
    515     public MessageStructureWrapper() {}
    516   }
    517 
    518   /** Listener for the message structure being fetched. */
    519   private final class MessageStructureFetchedListener
    520       implements ImapFolder.MessageRetrievalListener {
    521 
    522     private MessageStructureWrapper mMessageStructure;
    523 
    524     public MessageStructureFetchedListener() {}
    525 
    526     public MessageStructureWrapper getMessageStructure() {
    527       return mMessageStructure;
    528     }
    529 
    530     @Override
    531     public void messageRetrieved(Message message) {
    532       LogUtils.d(TAG, "Fetched message structure for " + message.getUid());
    533       LogUtils.d(TAG, "Message retrieved: " + message);
    534       try {
    535         mMessageStructure = getMessageOrNull(message);
    536         if (mMessageStructure == null) {
    537           LogUtils.d(TAG, "This voicemail does not have an attachment...");
    538           return;
    539         }
    540       } catch (MessagingException e) {
    541         LogUtils.e(TAG, e, "Messaging Exception");
    542         closeImapFolder();
    543       }
    544     }
    545 
    546     /**
    547      * Check if this IMAP message is a valid voicemail and whether it contains a transcription.
    548      *
    549      * @param message The IMAP message.
    550      * @return The MessageStructureWrapper object corresponding to an IMAP message and
    551      *     transcription.
    552      */
    553     private MessageStructureWrapper getMessageOrNull(Message message) throws MessagingException {
    554       if (!message.getMimeType().startsWith("multipart/")) {
    555         LogUtils.w(TAG, "Ignored non multi-part message");
    556         return null;
    557       }
    558 
    559       MessageStructureWrapper messageStructureWrapper = new MessageStructureWrapper();
    560 
    561       Multipart multipart = (Multipart) message.getBody();
    562       for (int i = 0; i < multipart.getCount(); ++i) {
    563         BodyPart bodyPart = multipart.getBodyPart(i);
    564         String bodyPartMimeType = bodyPart.getMimeType().toLowerCase();
    565         LogUtils.d(TAG, "bodyPart mime type: " + bodyPartMimeType);
    566 
    567         if (bodyPartMimeType.startsWith("audio/")) {
    568           messageStructureWrapper.messageStructure = message;
    569         } else if (bodyPartMimeType.startsWith("text/")) {
    570           messageStructureWrapper.transcriptionBodyPart = bodyPart;
    571         } else {
    572           VvmLog.v(TAG, "Unknown bodyPart MIME: " + bodyPartMimeType);
    573         }
    574       }
    575 
    576       if (messageStructureWrapper.messageStructure != null) {
    577         return messageStructureWrapper;
    578       }
    579 
    580       // No attachment found, this is not a voicemail.
    581       return null;
    582     }
    583   }
    584 
    585   /** Listener for the message body being fetched. */
    586   private final class MessageBodyFetchedListener implements ImapFolder.MessageRetrievalListener {
    587 
    588     private VoicemailPayload mVoicemailPayload;
    589 
    590     /** Returns the fetch voicemail payload. */
    591     public VoicemailPayload getVoicemailPayload() {
    592       return mVoicemailPayload;
    593     }
    594 
    595     @Override
    596     public void messageRetrieved(Message message) {
    597       LogUtils.d(TAG, "Fetched message body for " + message.getUid());
    598       LogUtils.d(TAG, "Message retrieved: " + message);
    599       try {
    600         mVoicemailPayload = getVoicemailPayloadFromMessage(message);
    601       } catch (MessagingException e) {
    602         LogUtils.e(TAG, "Messaging Exception:", e);
    603       } catch (IOException e) {
    604         LogUtils.e(TAG, "IO Exception:", e);
    605       }
    606     }
    607 
    608     private VoicemailPayload getVoicemailPayloadFromMessage(Message message)
    609         throws MessagingException, IOException {
    610       Multipart multipart = (Multipart) message.getBody();
    611       List<String> mimeTypes = new ArrayList<>();
    612       for (int i = 0; i < multipart.getCount(); ++i) {
    613         BodyPart bodyPart = multipart.getBodyPart(i);
    614         String bodyPartMimeType = bodyPart.getMimeType().toLowerCase();
    615         mimeTypes.add(bodyPartMimeType);
    616         if (bodyPartMimeType.startsWith("audio/")) {
    617           byte[] bytes = getDataFromBody(bodyPart.getBody());
    618           LogUtils.d(TAG, String.format("Fetched %s bytes of data", bytes.length));
    619           return new VoicemailPayload(bodyPartMimeType, bytes);
    620         }
    621       }
    622       LogUtils.e(TAG, "No audio attachment found on this voicemail, mimeTypes:" + mimeTypes);
    623       return null;
    624     }
    625   }
    626 
    627   /** Listener for the transcription being fetched. */
    628   private final class TranscriptionFetchedListener implements ImapFolder.MessageRetrievalListener {
    629 
    630     private String mVoicemailTranscription;
    631 
    632     /** Returns the fetched voicemail transcription. */
    633     public String getVoicemailTranscription() {
    634       return mVoicemailTranscription;
    635     }
    636 
    637     @Override
    638     public void messageRetrieved(Message message) {
    639       LogUtils.d(TAG, "Fetched transcription for " + message.getUid());
    640       try {
    641         mVoicemailTranscription = new String(getDataFromBody(message.getBody()));
    642       } catch (MessagingException e) {
    643         LogUtils.e(TAG, "Messaging Exception:", e);
    644       } catch (IOException e) {
    645         LogUtils.e(TAG, "IO Exception:", e);
    646       }
    647     }
    648   }
    649 
    650   private ImapFolder openImapFolder(String modeReadWrite) {
    651     try {
    652       if (mImapStore == null) {
    653         return null;
    654       }
    655       ImapFolder folder = new ImapFolder(mImapStore, ImapConstants.INBOX);
    656       folder.open(modeReadWrite);
    657       return folder;
    658     } catch (MessagingException e) {
    659       LogUtils.e(TAG, e, "Messaging Exception");
    660     }
    661     return null;
    662   }
    663 
    664   private Message[] convertToImapMessages(List<Voicemail> voicemails) {
    665     Message[] messages = new Message[voicemails.size()];
    666     for (int i = 0; i < voicemails.size(); ++i) {
    667       messages[i] = new MimeMessage();
    668       messages[i].setUid(voicemails.get(i).getSourceData());
    669     }
    670     return messages;
    671   }
    672 
    673   private void closeImapFolder() {
    674     if (mFolder != null) {
    675       mFolder.close(true);
    676     }
    677   }
    678 
    679   private byte[] getDataFromBody(Body body) throws IOException, MessagingException {
    680     ByteArrayOutputStream out = new ByteArrayOutputStream();
    681     BufferedOutputStream bufferedOut = new BufferedOutputStream(out);
    682     try {
    683       body.writeTo(bufferedOut);
    684       return Base64.decode(out.toByteArray(), Base64.DEFAULT);
    685     } finally {
    686       IOUtils.closeQuietly(bufferedOut);
    687       IOUtils.closeQuietly(out);
    688     }
    689   }
    690 }
    691