• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
InitializingException(String message)83     public InitializingException(String message) {
84       super(message);
85     }
86   }
87 
ImapHelper( Context context, PhoneAccountHandle phoneAccount, Network network, Editor status)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 
ImapHelper( Context context, OmtpVvmCarrierConfigHelper config, PhoneAccountHandle phoneAccount, Network network, Editor status)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
close()138   public void close() {
139     mImapStore.closeConnection();
140   }
141 
isRoaming()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 
getConfig()152   public OmtpVvmCarrierConfigHelper getConfig() {
153     return mConfig;
154   }
155 
connect()156   public ImapConnection connect() {
157     return mImapStore.getConnection();
158   }
159 
160   /** The caller thread will block until the method returns. */
markMessagesAsRead(List<Voicemail> voicemails)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. */
markMessagesAsDeleted(List<Voicemail> voicemails)166   public boolean markMessagesAsDeleted(List<Voicemail> voicemails) {
167     return setFlags(voicemails, Flag.DELETED);
168   }
169 
handleEvent(OmtpEvents event)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    */
setFlags(List<Voicemail> voicemails, String... flags)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    */
fetchAllVoicemails()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    */
getVoicemailFromMessageStructure( MessageStructureWrapper messageStructureWrapper)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    */
getNumber(Address[] fromAddresses)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    */
fetchMessageStructure(Message message)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 
fetchVoicemailPayload(VoicemailFetchedCallback callback, final String uid)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    */
fetchVoicemailPayload(Message message)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 
fetchTranscription(TranscriptionFetchedCallback callback, String uid)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
changePin(String oldPin, String newPin)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 
changeVoicemailTuiLanguage(String languageCode)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 
closeNewUserTutorial()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
getChangePinResultFromImapResponse(ImapResponse response)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 
updateQuota()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
getQuota()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 
updateQuota(ImapFolder folder)485   private void updateQuota(ImapFolder folder) throws MessagingException {
486     setQuota(folder.getQuota());
487   }
488 
setQuota(ImapFolder.Quota quota)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 
MessageStructureWrapper()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 
MessageStructureFetchedListener()524     public MessageStructureFetchedListener() {}
525 
getMessageStructure()526     public MessageStructureWrapper getMessageStructure() {
527       return mMessageStructure;
528     }
529 
530     @Override
messageRetrieved(Message message)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      */
getMessageOrNull(Message message)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. */
getVoicemailPayload()591     public VoicemailPayload getVoicemailPayload() {
592       return mVoicemailPayload;
593     }
594 
595     @Override
messageRetrieved(Message message)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 
getVoicemailPayloadFromMessage(Message message)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. */
getVoicemailTranscription()633     public String getVoicemailTranscription() {
634       return mVoicemailTranscription;
635     }
636 
637     @Override
messageRetrieved(Message message)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 
openImapFolder(String modeReadWrite)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 
convertToImapMessages(List<Voicemail> voicemails)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 
closeImapFolder()673   private void closeImapFolder() {
674     if (mFolder != null) {
675       mFolder.close(true);
676     }
677   }
678 
getDataFromBody(Body body)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