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 }