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.net.Network;
     20 import android.telecom.PhoneAccountHandle;
     21 import android.telecom.Voicemail;
     22 import android.telephony.TelephonyManager;
     23 import android.util.Base64;
     24 
     25 import com.android.phone.PhoneUtils;
     26 import com.android.phone.common.mail.Address;
     27 import com.android.phone.common.mail.Body;
     28 import com.android.phone.common.mail.BodyPart;
     29 import com.android.phone.common.mail.FetchProfile;
     30 import com.android.phone.common.mail.Flag;
     31 import com.android.phone.common.mail.Message;
     32 import com.android.phone.common.mail.MessagingException;
     33 import com.android.phone.common.mail.Multipart;
     34 import com.android.phone.common.mail.TempDirectory;
     35 import com.android.phone.common.mail.internet.MimeMessage;
     36 import com.android.phone.common.mail.store.ImapFolder;
     37 import com.android.phone.common.mail.store.ImapStore;
     38 import com.android.phone.common.mail.store.imap.ImapConstants;
     39 import com.android.phone.common.mail.utils.LogUtils;
     40 import com.android.phone.settings.VisualVoicemailSettingsUtil;
     41 import com.android.phone.vvm.omtp.OmtpConstants;
     42 import com.android.phone.vvm.omtp.OmtpVvmCarrierConfigHelper;
     43 import com.android.phone.vvm.omtp.fetch.VoicemailFetchedCallback;
     44 
     45 import libcore.io.IoUtils;
     46 
     47 import java.io.BufferedOutputStream;
     48 import java.io.ByteArrayOutputStream;
     49 import java.io.IOException;
     50 import java.util.ArrayList;
     51 import java.util.Arrays;
     52 import java.util.List;
     53 
     54 /**
     55  * A helper interface to abstract commands sent across IMAP interface for a given account.
     56  */
     57 public class ImapHelper {
     58     private final String TAG = "ImapHelper";
     59 
     60     private ImapFolder mFolder;
     61     private ImapStore mImapStore;
     62     private Context mContext;
     63     private PhoneAccountHandle mPhoneAccount;
     64 
     65     public ImapHelper(Context context, PhoneAccountHandle phoneAccount, Network network) {
     66         try {
     67             mContext = context;
     68             mPhoneAccount = phoneAccount;
     69             TempDirectory.setTempDirectory(context);
     70 
     71             String username = VisualVoicemailSettingsUtil.getVisualVoicemailCredentials(context,
     72                     OmtpConstants.IMAP_USER_NAME, phoneAccount);
     73             String password = VisualVoicemailSettingsUtil.getVisualVoicemailCredentials(context,
     74                     OmtpConstants.IMAP_PASSWORD, phoneAccount);
     75             String serverName = VisualVoicemailSettingsUtil.getVisualVoicemailCredentials(context,
     76                     OmtpConstants.SERVER_ADDRESS, phoneAccount);
     77             int port = Integer.parseInt(
     78                     VisualVoicemailSettingsUtil.getVisualVoicemailCredentials(context,
     79                             OmtpConstants.IMAP_PORT, phoneAccount));
     80             int auth = ImapStore.FLAG_NONE;
     81 
     82             OmtpVvmCarrierConfigHelper carrierConfigHelper = new OmtpVvmCarrierConfigHelper(context,
     83                     PhoneUtils.getSubIdForPhoneAccountHandle(phoneAccount));
     84             if (TelephonyManager.VVM_TYPE_CVVM.equals(carrierConfigHelper.getVvmType())) {
     85                 // TODO: move these into the carrier config app
     86                 port = 993;
     87                 auth = ImapStore.FLAG_SSL;
     88             }
     89 
     90             mImapStore = new ImapStore(
     91                     context, username, password, port, serverName, auth, network);
     92         } catch (NumberFormatException e) {
     93             LogUtils.w(TAG, "Could not parse port number");
     94         }
     95     }
     96 
     97     /**
     98      * If mImapStore is null, this means that there was a missing or badly formatted port number,
     99      * which means there aren't sufficient credentials for login. If mImapStore is succcessfully
    100      * initialized, then ImapHelper is ready to go.
    101      */
    102     public boolean isSuccessfullyInitialized() {
    103         return mImapStore != null;
    104     }
    105 
    106     /** The caller thread will block until the method returns. */
    107     public boolean markMessagesAsRead(List<Voicemail> voicemails) {
    108         return setFlags(voicemails, Flag.SEEN);
    109     }
    110 
    111     /** The caller thread will block until the method returns. */
    112     public boolean markMessagesAsDeleted(List<Voicemail> voicemails) {
    113         return setFlags(voicemails, Flag.DELETED);
    114     }
    115 
    116     /**
    117      * Set flags on the server for a given set of voicemails.
    118      *
    119      * @param voicemails The voicemails to set flags for.
    120      * @param flags The flags to set on the voicemails.
    121      * @return {@code true} if the operation completes successfully, {@code false} otherwise.
    122      */
    123     private boolean setFlags(List<Voicemail> voicemails, String... flags) {
    124         if (voicemails.size() == 0) {
    125             return false;
    126         }
    127         try {
    128             mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
    129             if (mFolder != null) {
    130                 mFolder.setFlags(convertToImapMessages(voicemails), flags, true);
    131                 return true;
    132             }
    133             return false;
    134         } catch (MessagingException e) {
    135             LogUtils.e(TAG, e, "Messaging exception");
    136             return false;
    137         } finally {
    138             closeImapFolder();
    139         }
    140     }
    141 
    142     /**
    143      * Fetch a list of voicemails from the server.
    144      *
    145      * @return A list of voicemail objects containing data about voicemails stored on the server.
    146      */
    147     public List<Voicemail> fetchAllVoicemails() {
    148         List<Voicemail> result = new ArrayList<Voicemail>();
    149         Message[] messages;
    150         try {
    151             mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
    152             if (mFolder == null) {
    153                 // This means we were unable to successfully open the folder.
    154                 return null;
    155             }
    156 
    157             // This method retrieves lightweight messages containing only the uid of the message.
    158             messages = mFolder.getMessages(null);
    159 
    160             for (Message message : messages) {
    161                 // Get the voicemail details.
    162                 Voicemail voicemail = fetchVoicemail(message);
    163                 if (voicemail != null) {
    164                     result.add(voicemail);
    165                 }
    166             }
    167             return result;
    168         } catch (MessagingException e) {
    169             LogUtils.e(TAG, e, "Messaging Exception");
    170             return null;
    171         } finally {
    172             closeImapFolder();
    173         }
    174     }
    175 
    176     /**
    177      * Fetches the structure of the given message and returns the voicemail parsed from it.
    178      *
    179      * @throws MessagingException if fetching the structure of the message fails
    180      */
    181     private Voicemail fetchVoicemail(Message message)
    182             throws MessagingException {
    183         LogUtils.d(TAG, "Fetching message structure for " + message.getUid());
    184 
    185         MessageStructureFetchedListener listener = new MessageStructureFetchedListener();
    186 
    187         FetchProfile fetchProfile = new FetchProfile();
    188         fetchProfile.addAll(Arrays.asList(FetchProfile.Item.FLAGS, FetchProfile.Item.ENVELOPE,
    189                 FetchProfile.Item.STRUCTURE));
    190 
    191         // The IMAP folder fetch method will call "messageRetrieved" on the listener when the
    192         // message is successfully retrieved.
    193         mFolder.fetch(new Message[] {message}, fetchProfile, listener);
    194         return listener.getVoicemail();
    195     }
    196 
    197 
    198     public boolean fetchVoicemailPayload(VoicemailFetchedCallback callback, final String uid) {
    199         Message message;
    200         try {
    201             mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
    202             if (mFolder == null) {
    203                 // This means we were unable to successfully open the folder.
    204                 return false;
    205             }
    206             message = mFolder.getMessage(uid);
    207             VoicemailPayload voicemailPayload = fetchVoicemailPayload(message);
    208 
    209             if (voicemailPayload == null) {
    210                 return false;
    211             }
    212 
    213             callback.setVoicemailContent(voicemailPayload);
    214             return true;
    215         } catch (MessagingException e) {
    216         } finally {
    217             closeImapFolder();
    218         }
    219         return false;
    220     }
    221 
    222     /**
    223      * Fetches the body of the given message and returns the parsed voicemail payload.
    224      *
    225      * @throws MessagingException if fetching the body of the message fails
    226      */
    227     private VoicemailPayload fetchVoicemailPayload(Message message)
    228             throws MessagingException {
    229         LogUtils.d(TAG, "Fetching message body for " + message.getUid());
    230 
    231         MessageBodyFetchedListener listener = new MessageBodyFetchedListener();
    232 
    233         FetchProfile fetchProfile = new FetchProfile();
    234         fetchProfile.add(FetchProfile.Item.BODY);
    235 
    236         mFolder.fetch(new Message[] {message}, fetchProfile, listener);
    237         return listener.getVoicemailPayload();
    238     }
    239 
    240     /**
    241      * Listener for the message structure being fetched.
    242      */
    243     private final class MessageStructureFetchedListener
    244             implements ImapFolder.MessageRetrievalListener {
    245         private Voicemail mVoicemail;
    246 
    247         public MessageStructureFetchedListener() {
    248         }
    249 
    250         public Voicemail getVoicemail() {
    251             return mVoicemail;
    252         }
    253 
    254         @Override
    255         public void messageRetrieved(Message message) {
    256             LogUtils.d(TAG, "Fetched message structure for " + message.getUid());
    257             LogUtils.d(TAG, "Message retrieved: " + message);
    258             try {
    259                 mVoicemail = getVoicemailFromMessage(message);
    260                 if (mVoicemail == null) {
    261                     LogUtils.d(TAG, "This voicemail does not have an attachment...");
    262                     return;
    263                 }
    264             } catch (MessagingException e) {
    265                 LogUtils.e(TAG, e, "Messaging Exception");
    266                 closeImapFolder();
    267             }
    268         }
    269 
    270         /**
    271          * Convert an IMAP message to a voicemail object.
    272          *
    273          * @param message The IMAP message.
    274          * @return The voicemail object corresponding to an IMAP message.
    275          * @throws MessagingException
    276          */
    277         private Voicemail getVoicemailFromMessage(Message message) throws MessagingException {
    278             if (!message.getMimeType().startsWith("multipart/")) {
    279                 LogUtils.w(TAG, "Ignored non multi-part message");
    280                 return null;
    281             }
    282 
    283             Multipart multipart = (Multipart) message.getBody();
    284             for (int i = 0; i < multipart.getCount(); ++i) {
    285                 BodyPart bodyPart = multipart.getBodyPart(i);
    286                 String bodyPartMimeType = bodyPart.getMimeType().toLowerCase();
    287                 LogUtils.d(TAG, "bodyPart mime type: " + bodyPartMimeType);
    288 
    289                 if (bodyPartMimeType.startsWith("audio/")) {
    290                     // Found an audio attachment, this is a valid voicemail.
    291                     long time = message.getSentDate().getTime();
    292                     String number = getNumber(message.getFrom());
    293                     boolean isRead = Arrays.asList(message.getFlags()).contains(Flag.SEEN);
    294 
    295                     return Voicemail.createForInsertion(time, number)
    296                             .setPhoneAccount(mPhoneAccount)
    297                             .setSourcePackage(mContext.getPackageName())
    298                             .setSourceData(message.getUid())
    299                             .setIsRead(isRead)
    300                             .build();
    301                 }
    302             }
    303             // No attachment found, this is not a voicemail.
    304             return null;
    305         }
    306 
    307         /**
    308          * The "from" field of a visual voicemail IMAP message is the number of the caller who left
    309          * the message. Extract this number from the list of "from" addresses.
    310          *
    311          * @param fromAddresses A list of addresses that comprise the "from" line.
    312          * @return The number of the voicemail sender.
    313          */
    314         private String getNumber(Address[] fromAddresses) {
    315             if (fromAddresses != null && fromAddresses.length > 0) {
    316                 if (fromAddresses.length != 1) {
    317                     LogUtils.w(TAG, "More than one from addresses found. Using the first one.");
    318                 }
    319                 String sender = fromAddresses[0].getAddress();
    320                 int atPos = sender.indexOf('@');
    321                 if (atPos != -1) {
    322                     // Strip domain part of the address.
    323                     sender = sender.substring(0, atPos);
    324                 }
    325                 return sender;
    326             }
    327             return null;
    328         }
    329     }
    330 
    331     /**
    332      * Listener for the message body being fetched.
    333      */
    334     private final class MessageBodyFetchedListener implements ImapFolder.MessageRetrievalListener {
    335         private VoicemailPayload mVoicemailPayload;
    336 
    337         /** Returns the fetch voicemail payload. */
    338         public VoicemailPayload getVoicemailPayload() {
    339             return mVoicemailPayload;
    340         }
    341 
    342         @Override
    343         public void messageRetrieved(Message message) {
    344             LogUtils.d(TAG, "Fetched message body for " + message.getUid());
    345             LogUtils.d(TAG, "Message retrieved: " + message);
    346             try {
    347                 mVoicemailPayload = getVoicemailPayloadFromMessage(message);
    348             } catch (MessagingException e) {
    349                 LogUtils.e(TAG, "Messaging Exception:", e);
    350             } catch (IOException e) {
    351                 LogUtils.e(TAG, "IO Exception:", e);
    352             }
    353         }
    354 
    355         private VoicemailPayload getVoicemailPayloadFromMessage(Message message)
    356                 throws MessagingException, IOException {
    357             Multipart multipart = (Multipart) message.getBody();
    358             for (int i = 0; i < multipart.getCount(); ++i) {
    359                 BodyPart bodyPart = multipart.getBodyPart(i);
    360                 String bodyPartMimeType = bodyPart.getMimeType().toLowerCase();
    361                 LogUtils.d(TAG, "bodyPart mime type: " + bodyPartMimeType);
    362 
    363                 if (bodyPartMimeType.startsWith("audio/")) {
    364                     byte[] bytes = getAudioDataFromBody(bodyPart.getBody());
    365                     LogUtils.d(TAG, String.format("Fetched %s bytes of data", bytes.length));
    366                     return new VoicemailPayload(bodyPartMimeType, bytes);
    367                 }
    368             }
    369             LogUtils.e(TAG, "No audio attachment found on this voicemail");
    370             return null;
    371         }
    372 
    373         private byte[] getAudioDataFromBody(Body body) throws IOException, MessagingException {
    374             ByteArrayOutputStream out = new ByteArrayOutputStream();
    375             BufferedOutputStream bufferedOut = new BufferedOutputStream(out);
    376             try {
    377                 body.writeTo(bufferedOut);
    378             } finally {
    379                 IoUtils.closeQuietly(bufferedOut);
    380             }
    381             return Base64.decode(out.toByteArray(), Base64.DEFAULT);
    382         }
    383     }
    384 
    385     private ImapFolder openImapFolder(String modeReadWrite) {
    386         try {
    387             if (mImapStore == null) {
    388                 return null;
    389             }
    390             ImapFolder folder = new ImapFolder(mImapStore, ImapConstants.INBOX);
    391             folder.open(modeReadWrite);
    392             return folder;
    393         } catch (MessagingException e) {
    394             LogUtils.e(TAG, e, "Messaging Exception");
    395         }
    396         return null;
    397     }
    398 
    399     private Message[] convertToImapMessages(List<Voicemail> voicemails) {
    400         Message[] messages = new Message[voicemails.size()];
    401         for (int i = 0; i < voicemails.size(); ++i) {
    402             messages[i] = new MimeMessage();
    403             messages[i].setUid(voicemails.get(i).getSourceData());
    404         }
    405         return messages;
    406     }
    407 
    408     private void closeImapFolder() {
    409         if (mFolder != null) {
    410             mFolder.close(true);
    411         }
    412     }
    413 }