Home | History | Annotate | Download | only in imap
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License
     15  */
     16 package com.android.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 
     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      */
    131     public boolean isSuccessfullyInitialized() {
    132         return mImapStore != null;
    133     }
    134 
    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. */
    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. */
    151     public boolean markMessagesAsDeleted(List<Voicemail> voicemails) {
    152         return setFlags(voicemails, Flag.DELETED);
    153     }
    154 
    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      */
    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      */
    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      */
    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      */
    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      */
    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 
    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      */
    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 
    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 
    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 
    387     private void updateQuota(ImapFolder folder) throws MessagingException {
    388         setQuota(folder.getQuota());
    389     }
    390 
    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 
    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 
    427         public MessageStructureFetchedListener() {
    428         }
    429 
    430         public MessageStructureWrapper getMessageStructure() {
    431             return mMessageStructure;
    432         }
    433 
    434         @Override
    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          */
    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. */
    496         public VoicemailPayload getVoicemailPayload() {
    497             return mVoicemailPayload;
    498         }
    499 
    500         @Override
    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 
    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. */
    540         public String getVoicemailTranscription() {
    541             return mVoicemailTranscription;
    542         }
    543 
    544         @Override
    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 
    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 
    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 
    580     private void closeImapFolder() {
    581         if (mFolder != null) {
    582             mFolder.close(true);
    583         }
    584     }
    585 
    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 
    598     private String getSharedPrefsKey(String key) {
    599         return VisualVoicemailSettingsUtil.getVisualVoicemailSharedPrefsKey(key, mPhoneAccount);
    600     }
    601 }