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.content.SharedPreferences;
20 import android.net.ConnectivityManager;
21 import android.net.Network;
22 import android.net.NetworkInfo;
23 import android.preference.PreferenceManager;
24 import android.provider.VoicemailContract;
25 import android.provider.VoicemailContract.Status;
26 import android.telecom.PhoneAccountHandle;
27 import android.telecom.Voicemail;
28 import android.telephony.TelephonyManager;
29 import android.util.Base64;
30 import android.util.Log;
31 
32 import com.android.phone.PhoneUtils;
33 import com.android.phone.VoicemailUtils;
34 import com.android.phone.common.mail.Address;
35 import com.android.phone.common.mail.Body;
36 import com.android.phone.common.mail.BodyPart;
37 import com.android.phone.common.mail.FetchProfile;
38 import com.android.phone.common.mail.Flag;
39 import com.android.phone.common.mail.Message;
40 import com.android.phone.common.mail.MessagingException;
41 import com.android.phone.common.mail.Multipart;
42 import com.android.phone.common.mail.TempDirectory;
43 import com.android.phone.common.mail.internet.MimeMessage;
44 import com.android.phone.common.mail.store.ImapFolder;
45 import com.android.phone.common.mail.store.ImapStore;
46 import com.android.phone.common.mail.store.imap.ImapConstants;
47 import com.android.phone.common.mail.utils.LogUtils;
48 import com.android.phone.settings.VisualVoicemailSettingsUtil;
49 import com.android.phone.vvm.omtp.OmtpConstants;
50 import com.android.phone.vvm.omtp.OmtpVvmCarrierConfigHelper;
51 import com.android.phone.vvm.omtp.fetch.VoicemailFetchedCallback;
52 import com.android.phone.vvm.omtp.sync.OmtpVvmSyncService.TranscriptionFetchedCallback;
53 
54 import libcore.io.IoUtils;
55 
56 import java.io.BufferedOutputStream;
57 import java.io.ByteArrayOutputStream;
58 import java.io.IOException;
59 import java.util.ArrayList;
60 import java.util.Arrays;
61 import java.util.List;
62 
63 /**
64  * A helper interface to abstract commands sent across IMAP interface for a given account.
65  */
66 public class ImapHelper {
67     private 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 
76     SharedPreferences mPrefs;
77     private static final String PREF_KEY_QUOTA_OCCUPIED = "quota_occupied_";
78     private static final String PREF_KEY_QUOTA_TOTAL = "quota_total_";
79 
80     private int mQuotaOccupied;
81     private int mQuotaTotal;
82 
ImapHelper(Context context, PhoneAccountHandle phoneAccount, Network network)83     public ImapHelper(Context context, PhoneAccountHandle phoneAccount, Network network) {
84         mContext = context;
85         mPhoneAccount = phoneAccount;
86         mNetwork = network;
87         try {
88             TempDirectory.setTempDirectory(context);
89 
90             String username = VisualVoicemailSettingsUtil.getVisualVoicemailCredentials(context,
91                     OmtpConstants.IMAP_USER_NAME, phoneAccount);
92             String password = VisualVoicemailSettingsUtil.getVisualVoicemailCredentials(context,
93                     OmtpConstants.IMAP_PASSWORD, phoneAccount);
94             String serverName = VisualVoicemailSettingsUtil.getVisualVoicemailCredentials(context,
95                     OmtpConstants.SERVER_ADDRESS, phoneAccount);
96             int port = Integer.parseInt(
97                     VisualVoicemailSettingsUtil.getVisualVoicemailCredentials(context,
98                             OmtpConstants.IMAP_PORT, phoneAccount));
99             int auth = ImapStore.FLAG_NONE;
100 
101             OmtpVvmCarrierConfigHelper carrierConfigHelper = new OmtpVvmCarrierConfigHelper(context,
102                     PhoneUtils.getSubIdForPhoneAccountHandle(phoneAccount));
103             if (TelephonyManager.VVM_TYPE_CVVM.equals(carrierConfigHelper.getVvmType())) {
104                 // TODO: move these into the carrier config app
105                 port = 993;
106                 auth = ImapStore.FLAG_SSL;
107             }
108 
109             mImapStore = new ImapStore(
110                     context, this, username, password, port, serverName, auth, network);
111         } catch (NumberFormatException e) {
112             VoicemailUtils.setDataChannelState(
113                     mContext, mPhoneAccount, Status.DATA_CHANNEL_STATE_BAD_CONFIGURATION);
114             LogUtils.w(TAG, "Could not parse port number");
115         }
116 
117         mPrefs = PreferenceManager.getDefaultSharedPreferences(context);
118         mQuotaOccupied = mPrefs.getInt(getSharedPrefsKey(PREF_KEY_QUOTA_OCCUPIED),
119                 VoicemailContract.Status.QUOTA_UNAVAILABLE);
120         mQuotaTotal = mPrefs.getInt(getSharedPrefsKey(PREF_KEY_QUOTA_TOTAL),
121                 VoicemailContract.Status.QUOTA_UNAVAILABLE);
122 
123         Log.v(TAG, "Quota:" + mQuotaOccupied + "/" + mQuotaTotal);
124     }
125 
126     /**
127      * If mImapStore is null, this means that there was a missing or badly formatted port number,
128      * which means there aren't sufficient credentials for login. If mImapStore is succcessfully
129      * initialized, then ImapHelper is ready to go.
130      */
isSuccessfullyInitialized()131     public boolean isSuccessfullyInitialized() {
132         return mImapStore != null;
133     }
134 
isRoaming()135     public boolean isRoaming(){
136         ConnectivityManager connectivityManager = (ConnectivityManager) mContext.getSystemService(
137                 Context.CONNECTIVITY_SERVICE);
138         NetworkInfo info = connectivityManager.getNetworkInfo(mNetwork);
139         if(info == null){
140             return false;
141         }
142         return info.isRoaming();
143     }
144 
145     /** The caller thread will block until the method returns. */
markMessagesAsRead(List<Voicemail> voicemails)146     public boolean markMessagesAsRead(List<Voicemail> voicemails) {
147         return setFlags(voicemails, Flag.SEEN);
148     }
149 
150     /** The caller thread will block until the method returns. */
markMessagesAsDeleted(List<Voicemail> voicemails)151     public boolean markMessagesAsDeleted(List<Voicemail> voicemails) {
152         return setFlags(voicemails, Flag.DELETED);
153     }
154 
setDataChannelState(int dataChannelState)155     public void setDataChannelState(int dataChannelState) {
156         VoicemailUtils.setDataChannelState(mContext, mPhoneAccount, dataChannelState);
157     }
158 
159     /**
160      * Set flags on the server for a given set of voicemails.
161      *
162      * @param voicemails The voicemails to set flags for.
163      * @param flags The flags to set on the voicemails.
164      * @return {@code true} if the operation completes successfully, {@code false} otherwise.
165      */
setFlags(List<Voicemail> voicemails, String... flags)166     private boolean setFlags(List<Voicemail> voicemails, String... flags) {
167         if (voicemails.size() == 0) {
168             return false;
169         }
170         try {
171             mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
172             if (mFolder != null) {
173                 mFolder.setFlags(convertToImapMessages(voicemails), flags, true);
174                 return true;
175             }
176             return false;
177         } catch (MessagingException e) {
178             LogUtils.e(TAG, e, "Messaging exception");
179             return false;
180         } finally {
181             closeImapFolder();
182         }
183     }
184 
185     /**
186      * Fetch a list of voicemails from the server.
187      *
188      * @return A list of voicemail objects containing data about voicemails stored on the server.
189      */
fetchAllVoicemails()190     public List<Voicemail> fetchAllVoicemails() {
191         List<Voicemail> result = new ArrayList<Voicemail>();
192         Message[] messages;
193         try {
194             mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
195             if (mFolder == null) {
196                 // This means we were unable to successfully open the folder.
197                 return null;
198             }
199 
200             // This method retrieves lightweight messages containing only the uid of the message.
201             messages = mFolder.getMessages(null);
202 
203             for (Message message : messages) {
204                 // Get the voicemail details (message structure).
205                 MessageStructureWrapper messageStructureWrapper = fetchMessageStructure(message);
206                 if (messageStructureWrapper != null) {
207                     result.add(getVoicemailFromMessageStructure(messageStructureWrapper));
208                 }
209             }
210             return result;
211         } catch (MessagingException e) {
212             LogUtils.e(TAG, e, "Messaging Exception");
213             return null;
214         } finally {
215             closeImapFolder();
216         }
217     }
218 
219     /**
220      * Extract voicemail details from the message structure. Also fetch transcription if a
221      * transcription exists.
222      */
getVoicemailFromMessageStructure( MessageStructureWrapper messageStructureWrapper)223     private Voicemail getVoicemailFromMessageStructure(
224             MessageStructureWrapper messageStructureWrapper) throws MessagingException{
225         Message messageDetails = messageStructureWrapper.messageStructure;
226 
227         TranscriptionFetchedListener listener = new TranscriptionFetchedListener();
228         if (messageStructureWrapper.transcriptionBodyPart != null) {
229             FetchProfile fetchProfile = new FetchProfile();
230             fetchProfile.add(messageStructureWrapper.transcriptionBodyPart);
231 
232             mFolder.fetch(new Message[] {messageDetails}, fetchProfile, listener);
233         }
234 
235         // Found an audio attachment, this is a valid voicemail.
236         long time = messageDetails.getSentDate().getTime();
237         String number = getNumber(messageDetails.getFrom());
238         boolean isRead = Arrays.asList(messageDetails.getFlags()).contains(Flag.SEEN);
239         return Voicemail.createForInsertion(time, number)
240                 .setPhoneAccount(mPhoneAccount)
241                 .setSourcePackage(mContext.getPackageName())
242                 .setSourceData(messageDetails.getUid())
243                 .setIsRead(isRead)
244                 .setTranscription(listener.getVoicemailTranscription())
245                 .build();
246     }
247 
248     /**
249      * The "from" field of a visual voicemail IMAP message is the number of the caller who left
250      * the message. Extract this number from the list of "from" addresses.
251      *
252      * @param fromAddresses A list of addresses that comprise the "from" line.
253      * @return The number of the voicemail sender.
254      */
getNumber(Address[] fromAddresses)255     private String getNumber(Address[] fromAddresses) {
256         if (fromAddresses != null && fromAddresses.length > 0) {
257             if (fromAddresses.length != 1) {
258                 LogUtils.w(TAG, "More than one from addresses found. Using the first one.");
259             }
260             String sender = fromAddresses[0].getAddress();
261             int atPos = sender.indexOf('@');
262             if (atPos != -1) {
263                 // Strip domain part of the address.
264                 sender = sender.substring(0, atPos);
265             }
266             return sender;
267         }
268         return null;
269     }
270 
271     /**
272      * Fetches the structure of the given message and returns a wrapper containing the message
273      * structure and the transcription structure (if applicable).
274      *
275      * @throws MessagingException if fetching the structure of the message fails
276      */
fetchMessageStructure(Message message)277     private MessageStructureWrapper fetchMessageStructure(Message message)
278             throws MessagingException {
279         LogUtils.d(TAG, "Fetching message structure for " + message.getUid());
280 
281         MessageStructureFetchedListener listener = new MessageStructureFetchedListener();
282 
283         FetchProfile fetchProfile = new FetchProfile();
284         fetchProfile.addAll(Arrays.asList(FetchProfile.Item.FLAGS, FetchProfile.Item.ENVELOPE,
285                 FetchProfile.Item.STRUCTURE));
286 
287         // The IMAP folder fetch method will call "messageRetrieved" on the listener when the
288         // message is successfully retrieved.
289         mFolder.fetch(new Message[] {message}, fetchProfile, listener);
290         return listener.getMessageStructure();
291     }
292 
fetchVoicemailPayload(VoicemailFetchedCallback callback, final String uid)293     public boolean fetchVoicemailPayload(VoicemailFetchedCallback callback, final String uid) {
294         try {
295             mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
296             if (mFolder == null) {
297                 // This means we were unable to successfully open the folder.
298                 return false;
299             }
300             Message message = mFolder.getMessage(uid);
301             if (message == null) {
302                 return false;
303             }
304             VoicemailPayload voicemailPayload = fetchVoicemailPayload(message);
305 
306             if (voicemailPayload == null) {
307                 return false;
308             }
309 
310             callback.setVoicemailContent(voicemailPayload);
311             return true;
312         } catch (MessagingException e) {
313         } finally {
314             closeImapFolder();
315         }
316         return false;
317     }
318 
319     /**
320      * Fetches the body of the given message and returns the parsed voicemail payload.
321      *
322      * @throws MessagingException if fetching the body of the message fails
323      */
fetchVoicemailPayload(Message message)324     private VoicemailPayload fetchVoicemailPayload(Message message)
325             throws MessagingException {
326         LogUtils.d(TAG, "Fetching message body for " + message.getUid());
327 
328         MessageBodyFetchedListener listener = new MessageBodyFetchedListener();
329 
330         FetchProfile fetchProfile = new FetchProfile();
331         fetchProfile.add(FetchProfile.Item.BODY);
332 
333         mFolder.fetch(new Message[] {message}, fetchProfile, listener);
334         return listener.getVoicemailPayload();
335     }
336 
fetchTranscription(TranscriptionFetchedCallback callback, String uid)337     public boolean fetchTranscription(TranscriptionFetchedCallback callback, String uid) {
338         try {
339             mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
340             if (mFolder == null) {
341                 // This means we were unable to successfully open the folder.
342                 return false;
343             }
344 
345             Message message = mFolder.getMessage(uid);
346             if (message == null) {
347                 return false;
348             }
349 
350             MessageStructureWrapper messageStructureWrapper = fetchMessageStructure(message);
351             if (messageStructureWrapper != null) {
352                 TranscriptionFetchedListener listener = new TranscriptionFetchedListener();
353                 if (messageStructureWrapper.transcriptionBodyPart != null) {
354                     FetchProfile fetchProfile = new FetchProfile();
355                     fetchProfile.add(messageStructureWrapper.transcriptionBodyPart);
356 
357                     // This method is called synchronously so the transcription will be populated
358                     // in the listener once the next method is called.
359                     mFolder.fetch(new Message[] {message}, fetchProfile, listener);
360                     callback.setVoicemailTranscription(listener.getVoicemailTranscription());
361                 }
362             }
363             return true;
364         } catch (MessagingException e) {
365             LogUtils.e(TAG, e, "Messaging Exception");
366             return false;
367         } finally {
368             closeImapFolder();
369         }
370     }
371 
updateQuota()372     public void updateQuota() {
373         try {
374             mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
375             if (mFolder == null) {
376                 // This means we were unable to successfully open the folder.
377                 return;
378             }
379             updateQuota(mFolder);
380         } catch (MessagingException e) {
381             LogUtils.e(TAG, e, "Messaging Exception");
382         } finally {
383             closeImapFolder();
384         }
385     }
386 
updateQuota(ImapFolder folder)387     private void updateQuota(ImapFolder folder) throws MessagingException {
388         setQuota(folder.getQuota());
389     }
390 
setQuota(ImapFolder.Quota quota)391     private void setQuota(ImapFolder.Quota quota) {
392         if (quota == null) {
393             return;
394         }
395         if (quota.occupied == mQuotaOccupied && quota.total == mQuotaTotal) {
396             Log.v(TAG, "Quota hasn't changed");
397             return;
398         }
399         mQuotaOccupied = quota.occupied;
400         mQuotaTotal = quota.total;
401         VoicemailContract.Status
402                 .setQuota(mContext, mPhoneAccount, mQuotaOccupied, mQuotaTotal);
403         mPrefs.edit()
404                 .putInt(getSharedPrefsKey(PREF_KEY_QUOTA_OCCUPIED), mQuotaOccupied)
405                 .putInt(getSharedPrefsKey(PREF_KEY_QUOTA_TOTAL), mQuotaTotal)
406                 .apply();
407         Log.v(TAG, "Quota changed to " + mQuotaOccupied + "/" + mQuotaTotal);
408     }
409     /**
410      * A wrapper to hold a message with its header details and the structure for transcriptions
411      * (so they can be fetched in the future).
412      */
413     public class MessageStructureWrapper {
414         public Message messageStructure;
415         public BodyPart transcriptionBodyPart;
416 
MessageStructureWrapper()417         public MessageStructureWrapper() { }
418     }
419 
420     /**
421      * Listener for the message structure being fetched.
422      */
423     private final class MessageStructureFetchedListener
424             implements ImapFolder.MessageRetrievalListener {
425         private MessageStructureWrapper mMessageStructure;
426 
MessageStructureFetchedListener()427         public MessageStructureFetchedListener() {
428         }
429 
getMessageStructure()430         public MessageStructureWrapper getMessageStructure() {
431             return mMessageStructure;
432         }
433 
434         @Override
messageRetrieved(Message message)435         public void messageRetrieved(Message message) {
436             LogUtils.d(TAG, "Fetched message structure for " + message.getUid());
437             LogUtils.d(TAG, "Message retrieved: " + message);
438             try {
439                 mMessageStructure = getMessageOrNull(message);
440                 if (mMessageStructure == null) {
441                     LogUtils.d(TAG, "This voicemail does not have an attachment...");
442                     return;
443                 }
444             } catch (MessagingException e) {
445                 LogUtils.e(TAG, e, "Messaging Exception");
446                 closeImapFolder();
447             }
448         }
449 
450         /**
451          * Check if this IMAP message is a valid voicemail and whether it contains a transcription.
452          *
453          * @param message The IMAP message.
454          * @return The MessageStructureWrapper object corresponding to an IMAP message and
455          * transcription.
456          * @throws MessagingException
457          */
getMessageOrNull(Message message)458         private MessageStructureWrapper getMessageOrNull(Message message)
459                 throws MessagingException {
460             if (!message.getMimeType().startsWith("multipart/")) {
461                 LogUtils.w(TAG, "Ignored non multi-part message");
462                 return null;
463             }
464 
465             MessageStructureWrapper messageStructureWrapper = new MessageStructureWrapper();
466 
467             Multipart multipart = (Multipart) message.getBody();
468             for (int i = 0; i < multipart.getCount(); ++i) {
469                 BodyPart bodyPart = multipart.getBodyPart(i);
470                 String bodyPartMimeType = bodyPart.getMimeType().toLowerCase();
471                 LogUtils.d(TAG, "bodyPart mime type: " + bodyPartMimeType);
472 
473                 if (bodyPartMimeType.startsWith("audio/")) {
474                     messageStructureWrapper.messageStructure = message;
475                 } else if (bodyPartMimeType.startsWith("text/")) {
476                     messageStructureWrapper.transcriptionBodyPart = bodyPart;
477                 }
478             }
479 
480             if (messageStructureWrapper.messageStructure != null) {
481                 return messageStructureWrapper;
482             }
483 
484             // No attachment found, this is not a voicemail.
485             return null;
486         }
487     }
488 
489     /**
490      * Listener for the message body being fetched.
491      */
492     private final class MessageBodyFetchedListener implements ImapFolder.MessageRetrievalListener {
493         private VoicemailPayload mVoicemailPayload;
494 
495         /** Returns the fetch voicemail payload. */
getVoicemailPayload()496         public VoicemailPayload getVoicemailPayload() {
497             return mVoicemailPayload;
498         }
499 
500         @Override
messageRetrieved(Message message)501         public void messageRetrieved(Message message) {
502             LogUtils.d(TAG, "Fetched message body for " + message.getUid());
503             LogUtils.d(TAG, "Message retrieved: " + message);
504             try {
505                 mVoicemailPayload = getVoicemailPayloadFromMessage(message);
506             } catch (MessagingException e) {
507                 LogUtils.e(TAG, "Messaging Exception:", e);
508             } catch (IOException e) {
509                 LogUtils.e(TAG, "IO Exception:", e);
510             }
511         }
512 
getVoicemailPayloadFromMessage(Message message)513         private VoicemailPayload getVoicemailPayloadFromMessage(Message message)
514                 throws MessagingException, IOException {
515             Multipart multipart = (Multipart) message.getBody();
516             for (int i = 0; i < multipart.getCount(); ++i) {
517                 BodyPart bodyPart = multipart.getBodyPart(i);
518                 String bodyPartMimeType = bodyPart.getMimeType().toLowerCase();
519                 LogUtils.d(TAG, "bodyPart mime type: " + bodyPartMimeType);
520 
521                 if (bodyPartMimeType.startsWith("audio/")) {
522                     byte[] bytes = getDataFromBody(bodyPart.getBody());
523                     LogUtils.d(TAG, String.format("Fetched %s bytes of data", bytes.length));
524                     return new VoicemailPayload(bodyPartMimeType, bytes);
525                 }
526             }
527             LogUtils.e(TAG, "No audio attachment found on this voicemail");
528             return null;
529         }
530     }
531 
532     /**
533      * Listener for the transcription being fetched.
534      */
535     private final class TranscriptionFetchedListener implements
536             ImapFolder.MessageRetrievalListener {
537         private String mVoicemailTranscription;
538 
539         /** Returns the fetched voicemail transcription. */
getVoicemailTranscription()540         public String getVoicemailTranscription() {
541             return mVoicemailTranscription;
542         }
543 
544         @Override
messageRetrieved(Message message)545         public void messageRetrieved(Message message) {
546             LogUtils.d(TAG, "Fetched transcription for " + message.getUid());
547             try {
548                 mVoicemailTranscription = new String(getDataFromBody(message.getBody()));
549             } catch (MessagingException e) {
550                 LogUtils.e(TAG, "Messaging Exception:", e);
551             } catch (IOException e) {
552                 LogUtils.e(TAG, "IO Exception:", e);
553             }
554         }
555     }
556 
openImapFolder(String modeReadWrite)557     private ImapFolder openImapFolder(String modeReadWrite) {
558         try {
559             if (mImapStore == null) {
560                 return null;
561             }
562             ImapFolder folder = new ImapFolder(mImapStore, ImapConstants.INBOX);
563             folder.open(modeReadWrite);
564             return folder;
565         } catch (MessagingException e) {
566             LogUtils.e(TAG, e, "Messaging Exception");
567         }
568         return null;
569     }
570 
convertToImapMessages(List<Voicemail> voicemails)571     private Message[] convertToImapMessages(List<Voicemail> voicemails) {
572         Message[] messages = new Message[voicemails.size()];
573         for (int i = 0; i < voicemails.size(); ++i) {
574             messages[i] = new MimeMessage();
575             messages[i].setUid(voicemails.get(i).getSourceData());
576         }
577         return messages;
578     }
579 
closeImapFolder()580     private void closeImapFolder() {
581         if (mFolder != null) {
582             mFolder.close(true);
583         }
584     }
585 
getDataFromBody(Body body)586     private byte[] getDataFromBody(Body body) throws IOException, MessagingException {
587         ByteArrayOutputStream out = new ByteArrayOutputStream();
588         BufferedOutputStream bufferedOut = new BufferedOutputStream(out);
589         try {
590             body.writeTo(bufferedOut);
591             return Base64.decode(out.toByteArray(), Base64.DEFAULT);
592         } finally {
593             IoUtils.closeQuietly(bufferedOut);
594             IoUtils.closeQuietly(out);
595         }
596     }
597 
getSharedPrefsKey(String key)598     private String getSharedPrefsKey(String key) {
599         return VisualVoicemailSettingsUtil.getVisualVoicemailSharedPrefsKey(key, mPhoneAccount);
600     }
601 }