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.PinChanger;
26 import com.android.voicemail.PinChanger.ChangePinResult;
27 import com.android.voicemail.impl.OmtpConstants;
28 import com.android.voicemail.impl.OmtpEvents;
29 import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
30 import com.android.voicemail.impl.VisualVoicemailPreferences;
31 import com.android.voicemail.impl.Voicemail;
32 import com.android.voicemail.impl.VoicemailStatus;
33 import com.android.voicemail.impl.VoicemailStatus.Editor;
34 import com.android.voicemail.impl.VvmLog;
35 import com.android.voicemail.impl.fetch.VoicemailFetchedCallback;
36 import com.android.voicemail.impl.mail.Address;
37 import com.android.voicemail.impl.mail.Body;
38 import com.android.voicemail.impl.mail.BodyPart;
39 import com.android.voicemail.impl.mail.FetchProfile;
40 import com.android.voicemail.impl.mail.Flag;
41 import com.android.voicemail.impl.mail.Message;
42 import com.android.voicemail.impl.mail.MessagingException;
43 import com.android.voicemail.impl.mail.Multipart;
44 import com.android.voicemail.impl.mail.TempDirectory;
45 import com.android.voicemail.impl.mail.internet.MimeMessage;
46 import com.android.voicemail.impl.mail.store.ImapConnection;
47 import com.android.voicemail.impl.mail.store.ImapFolder;
48 import com.android.voicemail.impl.mail.store.ImapFolder.Quota;
49 import com.android.voicemail.impl.mail.store.ImapStore;
50 import com.android.voicemail.impl.mail.store.imap.ImapConstants;
51 import com.android.voicemail.impl.mail.store.imap.ImapResponse;
52 import com.android.voicemail.impl.mail.utils.LogUtils;
53 import com.android.voicemail.impl.sync.OmtpVvmSyncService.TranscriptionFetchedCallback;
54 import java.io.BufferedOutputStream;
55 import java.io.ByteArrayOutputStream;
56 import java.io.Closeable;
57 import java.io.IOException;
58 import java.util.ArrayList;
59 import java.util.Arrays;
60 import java.util.List;
61 import java.util.Locale;
62 import org.apache.commons.io.IOUtils;
63 
64 /** A helper interface to abstract commands sent across IMAP interface for a given account. */
65 public class ImapHelper implements Closeable {
66 
67   private static final String TAG = "ImapHelper";
68 
69   private ImapFolder folder;
70   private ImapStore imapStore;
71 
72   private final Context context;
73   private final PhoneAccountHandle phoneAccount;
74   private final Network network;
75   private final Editor status;
76 
77   VisualVoicemailPreferences prefs;
78 
79   private final OmtpVvmCarrierConfigHelper config;
80 
81   /** InitializingException */
82   public static class InitializingException extends Exception {
83 
InitializingException(String message)84     public InitializingException(String message) {
85       super(message);
86     }
87   }
88 
ImapHelper( Context context, PhoneAccountHandle phoneAccount, Network network, Editor status)89   public ImapHelper(
90       Context context, PhoneAccountHandle phoneAccount, Network network, Editor status)
91       throws InitializingException {
92     this(
93         context,
94         new OmtpVvmCarrierConfigHelper(context, phoneAccount),
95         phoneAccount,
96         network,
97         status);
98   }
99 
ImapHelper( Context context, OmtpVvmCarrierConfigHelper config, PhoneAccountHandle phoneAccount, Network network, Editor status)100   public ImapHelper(
101       Context context,
102       OmtpVvmCarrierConfigHelper config,
103       PhoneAccountHandle phoneAccount,
104       Network network,
105       Editor status)
106       throws InitializingException {
107     this.context = context;
108     this.phoneAccount = phoneAccount;
109     this.network = network;
110     this.status = status;
111     this.config = config;
112     prefs = new VisualVoicemailPreferences(context, phoneAccount);
113 
114     try {
115       TempDirectory.setTempDirectory(context);
116 
117       String username = prefs.getString(OmtpConstants.IMAP_USER_NAME, null);
118       String password = prefs.getString(OmtpConstants.IMAP_PASSWORD, null);
119       String serverName = prefs.getString(OmtpConstants.SERVER_ADDRESS, null);
120       int port = Integer.parseInt(prefs.getString(OmtpConstants.IMAP_PORT, null));
121       int auth = ImapStore.FLAG_NONE;
122 
123       int sslPort = this.config.getSslPort();
124       if (sslPort != 0) {
125         port = sslPort;
126         auth = ImapStore.FLAG_SSL;
127       }
128 
129       imapStore = 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     imapStore.closeConnection();
140   }
141 
isRoaming()142   public boolean isRoaming() {
143     ConnectivityManager connectivityManager =
144         (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
145     NetworkInfo info = connectivityManager.getNetworkInfo(network);
146     if (info == null) {
147       return false;
148     }
149     return info.isRoaming();
150   }
151 
getConfig()152   public OmtpVvmCarrierConfigHelper getConfig() {
153     return config;
154   }
155 
connect()156   public ImapConnection connect() {
157     return imapStore.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     config.handleEvent(status, 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       folder = openImapFolder(ImapFolder.MODE_READ_WRITE);
187       if (folder != null) {
188         folder.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       folder = openImapFolder(ImapFolder.MODE_READ_WRITE);
210       if (folder == 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 = folder.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       folder.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(phoneAccount)
258             .setSourcePackage(context.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    */
getNumber(Address[] fromAddresses)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    */
fetchMessageStructure(Message message)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     folder.fetch(new Message[] {message}, fetchProfile, listener);
310     return listener.getMessageStructure();
311   }
312 
fetchVoicemailPayload(VoicemailFetchedCallback callback, final String uid)313   public boolean fetchVoicemailPayload(VoicemailFetchedCallback callback, final String uid) {
314     try {
315       folder = openImapFolder(ImapFolder.MODE_READ_WRITE);
316       if (folder == null) {
317         // This means we were unable to successfully open the folder.
318         return false;
319       }
320       Message message = folder.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    */
fetchVoicemailPayload(Message message)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     folder.fetch(new Message[] {message}, fetchProfile, listener);
348     return listener.getVoicemailPayload();
349   }
350 
fetchTranscription(TranscriptionFetchedCallback callback, String uid)351   public boolean fetchTranscription(TranscriptionFetchedCallback callback, String uid) {
352     try {
353       folder = openImapFolder(ImapFolder.MODE_READ_WRITE);
354       if (folder == null) {
355         // This means we were unable to successfully open the folder.
356         return false;
357       }
358 
359       Message message = folder.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           folder.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
changePin(String oldPin, String newPin)387   public int changePin(String oldPin, String newPin) throws MessagingException {
388     ImapConnection connection = imapStore.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 PinChanger.CHANGE_PIN_SYSTEM_ERROR;
397     } finally {
398       connection.destroyResponses();
399     }
400   }
401 
changeVoicemailTuiLanguage(String languageCode)402   public void changeVoicemailTuiLanguage(String languageCode) throws MessagingException {
403     ImapConnection connection = imapStore.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 
closeNewUserTutorial()415   public void closeNewUserTutorial() throws MessagingException {
416     ImapConnection connection = imapStore.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
getChangePinResultFromImapResponse(ImapResponse response)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 PinChanger.CHANGE_PIN_TOO_SHORT;
438       }
439       if (OmtpConstants.RESPONSE_CHANGE_PIN_TOO_LONG.equals(message)) {
440         return PinChanger.CHANGE_PIN_TOO_LONG;
441       }
442       if (OmtpConstants.RESPONSE_CHANGE_PIN_TOO_WEAK.equals(message)) {
443         return PinChanger.CHANGE_PIN_TOO_WEAK;
444       }
445       if (OmtpConstants.RESPONSE_CHANGE_PIN_MISMATCH.equals(message)) {
446         return PinChanger.CHANGE_PIN_MISMATCH;
447       }
448       if (OmtpConstants.RESPONSE_CHANGE_PIN_INVALID_CHARACTER.equals(message)) {
449         return PinChanger.CHANGE_PIN_INVALID_CHARACTER;
450       }
451       return PinChanger.CHANGE_PIN_SYSTEM_ERROR;
452     }
453     LogUtils.d(TAG, "change PIN succeeded");
454     return PinChanger.CHANGE_PIN_SUCCESS;
455   }
456 
updateQuota()457   public void updateQuota() {
458     try {
459       folder = openImapFolder(ImapFolder.MODE_READ_WRITE);
460       if (folder == null) {
461         // This means we were unable to successfully open the folder.
462         return;
463       }
464       updateQuota(folder);
465     } catch (MessagingException e) {
466       LogUtils.e(TAG, e, "Messaging Exception");
467     } finally {
468       closeImapFolder();
469     }
470   }
471 
472   @Nullable
getQuota()473   public Quota getQuota() {
474     try {
475       folder = openImapFolder(ImapFolder.MODE_READ_ONLY);
476       if (folder == 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 folder.getQuota();
482     } catch (MessagingException e) {
483       LogUtils.e(TAG, e, "Messaging Exception");
484       return null;
485     } finally {
486       closeImapFolder();
487     }
488   }
489 
updateQuota(ImapFolder folder)490   private void updateQuota(ImapFolder folder) throws MessagingException {
491     setQuota(folder.getQuota());
492   }
493 
setQuota(ImapFolder.Quota quota)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(context, phoneAccount).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 
MessageStructureWrapper()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 messageStructure;
528 
MessageStructureFetchedListener()529     public MessageStructureFetchedListener() {}
530 
getMessageStructure()531     public MessageStructureWrapper getMessageStructure() {
532       return messageStructure;
533     }
534 
535     @Override
messageRetrieved(Message message)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         messageStructure = getMessageOrNull(message);
541         if (messageStructure == 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      */
getMessageOrNull(Message message)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 (!config.ignoreTranscription() && 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 voicemailPayload;
594 
595     /** Returns the fetch voicemail payload. */
getVoicemailPayload()596     public VoicemailPayload getVoicemailPayload() {
597       return voicemailPayload;
598     }
599 
600     @Override
messageRetrieved(Message message)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         voicemailPayload = 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 
getVoicemailPayloadFromMessage(Message message)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 voicemailTranscription;
636 
637     /** Returns the fetched voicemail transcription. */
getVoicemailTranscription()638     public String getVoicemailTranscription() {
639       return voicemailTranscription;
640     }
641 
642     @Override
messageRetrieved(Message message)643     public void messageRetrieved(Message message) {
644       LogUtils.d(TAG, "Fetched transcription for " + message.getUid());
645       try {
646         voicemailTranscription = 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 
openImapFolder(String modeReadWrite)655   private ImapFolder openImapFolder(String modeReadWrite) {
656     try {
657       if (imapStore == null) {
658         return null;
659       }
660       ImapFolder folder = new ImapFolder(imapStore, 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 
convertToImapMessages(List<Voicemail> voicemails)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 
closeImapFolder()678   private void closeImapFolder() {
679     if (folder != null) {
680       folder.close(true);
681     }
682   }
683 
getDataFromBody(Body body)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