• 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.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 
InitializingException(String message)88         public InitializingException(String message) {
89             super(message);
90         }
91     }
92 
ImapHelper(Context context, PhoneAccountHandle phoneAccount, Network network, VoicemailStatus.Editor status)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 
ImapHelper(Context context, OmtpVvmCarrierConfigHelper config, PhoneAccountHandle phoneAccount, Network network, VoicemailStatus.Editor status)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
close()142     public void close() {
143         mImapStore.closeConnection();
144     }
145 
isRoaming()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 
getConfig()156     public OmtpVvmCarrierConfigHelper getConfig() {
157         return mConfig;
158     }
159 
connect()160     public ImapConnection connect() {
161         return mImapStore.getConnection();
162     }
163 
164     /**
165      * The caller thread will block until the method returns.
166      */
markMessagesAsRead(List<Voicemail> voicemails)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      */
markMessagesAsDeleted(List<Voicemail> voicemails)174     public boolean markMessagesAsDeleted(List<Voicemail> voicemails) {
175         return setFlags(voicemails, Flag.DELETED);
176     }
177 
handleEvent(OmtpEvents event)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      */
setFlags(List<Voicemail> voicemails, String... flags)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      */
fetchAllVoicemails()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      */
getVoicemailFromMessageStructure( MessageStructureWrapper messageStructureWrapper)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      */
getNumber(Address[] fromAddresses)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      */
fetchMessageStructure(Message message)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 
fetchVoicemailPayload(VoicemailFetchedCallback callback, final String uid)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      */
fetchVoicemailPayload(Message message)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 
fetchTranscription(TranscriptionFetchedCallback callback, String uid)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
changePin(String oldPin, String newPin)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 
changeVoicemailTuiLanguage(String languageCode)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 
closeNewUserTutorial()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
getChangePinResultFromImapResponse(ImapResponse response)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 
updateQuota()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 
updateQuota(ImapFolder folder)483     private void updateQuota(ImapFolder folder) throws MessagingException {
484         setQuota(folder.getQuota());
485     }
486 
setQuota(ImapFolder.Quota quota)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 
MessageStructureWrapper()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 
MessageStructureFetchedListener()528         public MessageStructureFetchedListener() {
529         }
530 
getMessageStructure()531         public MessageStructureWrapper getMessageStructure() {
532             return mMessageStructure;
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                 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          */
getMessageOrNull(Message message)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          */
getVoicemailPayload()601         public VoicemailPayload getVoicemailPayload() {
602             return mVoicemailPayload;
603         }
604 
605         @Override
messageRetrieved(Message message)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 
getVoicemailPayloadFromMessage(Message message)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          */
getVoicemailTranscription()648         public String getVoicemailTranscription() {
649             return mVoicemailTranscription;
650         }
651 
652         @Override
messageRetrieved(Message message)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 
openImapFolder(String modeReadWrite)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 
convertToImapMessages(List<Voicemail> voicemails)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 
closeImapFolder()688     private void closeImapFolder() {
689         if (mFolder != null) {
690             mFolder.close(true);
691         }
692     }
693 
getDataFromBody(Body body)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 }