• 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.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 
ImapHelper(Context context, PhoneAccountHandle phoneAccount, Network network)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      */
isSuccessfullyInitialized()102     public boolean isSuccessfullyInitialized() {
103         return mImapStore != null;
104     }
105 
106     /** The caller thread will block until the method returns. */
markMessagesAsRead(List<Voicemail> voicemails)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. */
markMessagesAsDeleted(List<Voicemail> voicemails)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      */
setFlags(List<Voicemail> voicemails, String... flags)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      */
fetchAllVoicemails()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      */
fetchVoicemail(Message message)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 
fetchVoicemailPayload(VoicemailFetchedCallback callback, final String uid)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      */
fetchVoicemailPayload(Message message)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 
MessageStructureFetchedListener()247         public MessageStructureFetchedListener() {
248         }
249 
getVoicemail()250         public Voicemail getVoicemail() {
251             return mVoicemail;
252         }
253 
254         @Override
messageRetrieved(Message message)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          */
getVoicemailFromMessage(Message message)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          */
getNumber(Address[] fromAddresses)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. */
getVoicemailPayload()338         public VoicemailPayload getVoicemailPayload() {
339             return mVoicemailPayload;
340         }
341 
342         @Override
messageRetrieved(Message message)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 
getVoicemailPayloadFromMessage(Message message)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 
getAudioDataFromBody(Body body)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 
openImapFolder(String modeReadWrite)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 
convertToImapMessages(List<Voicemail> voicemails)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 
closeImapFolder()408     private void closeImapFolder() {
409         if (mFolder != null) {
410             mFolder.close(true);
411         }
412     }
413 }