1 /* 2 * Copyright (C) 2008 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 17 package com.android.email.activity; 18 19 import com.android.email.Account; 20 import com.android.email.Email; 21 import com.android.email.LegacyConversions; 22 import com.android.email.Preferences; 23 import com.android.email.R; 24 import com.android.email.Utility; 25 import com.android.email.activity.setup.AccountSettingsUtils; 26 import com.android.email.activity.setup.AccountSettingsUtils.Provider; 27 import com.android.email.mail.FetchProfile; 28 import com.android.email.mail.Flag; 29 import com.android.email.mail.Folder; 30 import com.android.email.mail.Message; 31 import com.android.email.mail.MessagingException; 32 import com.android.email.mail.Part; 33 import com.android.email.mail.Store; 34 import com.android.email.mail.internet.MimeUtility; 35 import com.android.email.mail.store.LocalStore; 36 import com.android.email.provider.EmailContent; 37 import com.android.email.provider.EmailContent.AccountColumns; 38 import com.android.email.provider.EmailContent.HostAuth; 39 import com.android.email.provider.EmailContent.Mailbox; 40 41 import android.app.ListActivity; 42 import android.content.Context; 43 import android.content.Intent; 44 import android.os.AsyncTask; 45 import android.os.Bundle; 46 import android.os.Handler; 47 import android.util.Log; 48 import android.view.LayoutInflater; 49 import android.view.View; 50 import android.view.ViewGroup; 51 import android.view.View.OnClickListener; 52 import android.widget.BaseAdapter; 53 import android.widget.Button; 54 import android.widget.ListView; 55 import android.widget.ProgressBar; 56 import android.widget.TextView; 57 58 import java.io.IOException; 59 import java.net.URI; 60 import java.net.URISyntaxException; 61 import java.util.ArrayList; 62 import java.util.HashSet; 63 64 /** 65 * This activity will be used whenever we have a large/slow bulk upgrade operation. 66 * 67 * The general strategy is to iterate through the legacy accounts, convert them one-by-one, and 68 * then delete them. The loop is very conservative; If there is any problem, the bias will be 69 * to abandon the conversion and let the account be deleted. We never want to get stuck here, and 70 * we never want to run more than once (on a device being upgraded from 1.6). After this code 71 * runs, there should be zero legacy accounts. 72 * 73 * Note: It's preferable to check for "accounts needing upgrade" before launching this 74 * activity, so as to not waste time before every launch. 75 * 76 * Note: This activity is set (in the manifest) to disregard configuration changes (e.g. rotation). 77 * This allows it to continue through without restarting. 78 * Do not attempt to define orientation-specific resources, they won't be loaded. 79 * 80 * TODO: Read pending events and convert them to things like updates or deletes in the DB 81 * TODO: Smarter cleanup of SSL/TLS situation, since certificates may be bad (see design spec) 82 */ 83 public class UpgradeAccounts extends ListActivity implements OnClickListener { 84 85 /** DO NOT CHECK IN AS 'TRUE' - DEVELOPMENT ONLY */ 86 private static final boolean DEBUG_FORCE_UPGRADES = false; 87 88 private AccountInfo[] mLegacyAccounts; 89 private UIHandler mHandler = new UIHandler(); 90 private AccountsAdapter mAdapter; 91 private ListView mListView; 92 private Button mProceedButton; 93 private ConversionTask mConversionTask; 94 95 // These are used to hold off restart of this activity while worker is still busy 96 private static final Object sConversionInProgress = new Object(); 97 private static boolean sConversionHasRun = false; 98 99 /** This projection is for looking up accounts by their legacy UUID */ 100 private static final String WHERE_ACCOUNT_UUID_IS = AccountColumns.COMPATIBILITY_UUID + "=?"; 101 102 public static void actionStart(Context context) { 103 Intent i = new Intent(context, UpgradeAccounts.class); 104 context.startActivity(i); 105 } 106 107 @Override 108 public void onCreate(Bundle icicle) { 109 super.onCreate(icicle); 110 111 Preferences p = Preferences.getPreferences(this); 112 Account[] legacyAccounts = p.getAccounts(); 113 if (legacyAccounts.length == 0) { 114 finish(); 115 return; 116 } 117 loadAccountInfoArray(legacyAccounts); 118 119 Log.d(Email.LOG_TAG, "*** Preparing to upgrade " + 120 Integer.toString(mLegacyAccounts.length) + " accounts"); 121 122 setContentView(R.layout.upgrade_accounts); 123 mListView = getListView(); 124 mProceedButton = (Button) findViewById(R.id.action_button); 125 mProceedButton.setEnabled(false); 126 mProceedButton.setOnClickListener(this); 127 } 128 129 @Override 130 protected void onResume() { 131 super.onResume(); 132 updateList(); 133 134 // Start the big conversion engine 135 mConversionTask = new ConversionTask(mLegacyAccounts); 136 mConversionTask.execute(); 137 } 138 139 @Override 140 protected void onDestroy() { 141 super.onDestroy(); 142 143 Utility.cancelTask(mConversionTask, false); // false = Don't interrupt running task 144 mConversionTask = null; 145 } 146 147 /** 148 * Stopgap measure to prevent monkey or zealous user from exiting while we're still at work. 149 */ 150 @Override 151 public void onBackPressed() { 152 if (!mProceedButton.isEnabled()) { 153 finish(); 154 } 155 } 156 157 public void onClick(View v) { 158 switch (v.getId()) { 159 case R.id.action_button: 160 onClickOk(); 161 break; 162 } 163 } 164 165 private void onClickOk() { 166 Welcome.actionStart(UpgradeAccounts.this); 167 finish(); 168 } 169 170 private void updateList() { 171 mAdapter = new AccountsAdapter(); 172 getListView().setAdapter(mAdapter); 173 } 174 175 private static class AccountInfo { 176 Account account; 177 int maxProgress; 178 int progress; 179 String errorMessage; // set/read by handler - UI thread only 180 boolean isError; // set/read by worker thread 181 182 public AccountInfo(Account legacyAccount) { 183 account = legacyAccount; 184 maxProgress = 0; 185 progress = 0; 186 errorMessage = null; 187 isError = false; 188 } 189 } 190 191 private void loadAccountInfoArray(Account[] legacyAccounts) { 192 mLegacyAccounts = new AccountInfo[legacyAccounts.length]; 193 for (int i = 0; i < legacyAccounts.length; i++) { 194 AccountInfo ai = new AccountInfo(legacyAccounts[i]); 195 mLegacyAccounts[i] = ai; 196 } 197 } 198 199 private static class ViewHolder { 200 TextView displayName; 201 ProgressBar progress; 202 TextView errorReport; 203 } 204 205 class AccountsAdapter extends BaseAdapter { 206 final LayoutInflater mInflater; 207 208 AccountsAdapter() { 209 mInflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE); 210 } 211 212 @Override 213 public boolean hasStableIds() { 214 return true; 215 } 216 217 public int getCount() { 218 return mLegacyAccounts.length; 219 } 220 221 public Object getItem(int position) { 222 return mLegacyAccounts[position]; 223 } 224 225 public long getItemId(int position) { 226 return position; 227 } 228 229 public View getView(int position, View convertView, ViewGroup parent) { 230 View v; 231 if (convertView == null) { 232 v = newView(parent); 233 } else { 234 v = convertView; 235 } 236 bindView(v, position); 237 return v; 238 } 239 240 public View newView(ViewGroup parent) { 241 View v = mInflater.inflate(R.layout.upgrade_accounts_item, parent, false); 242 ViewHolder h = new ViewHolder(); 243 h.displayName = (TextView) v.findViewById(R.id.name); 244 h.progress = (ProgressBar) v.findViewById(R.id.progress); 245 h.errorReport = (TextView) v.findViewById(R.id.error); 246 v.setTag(h); 247 return v; 248 } 249 250 public void bindView(View view, int position) { 251 ViewHolder vh = (ViewHolder) view.getTag(); 252 AccountInfo ai = mLegacyAccounts[position]; 253 vh.displayName.setText(ai.account.getDescription()); 254 if (ai.errorMessage == null) { 255 vh.errorReport.setVisibility(View.GONE); 256 vh.progress.setVisibility(View.VISIBLE); 257 vh.progress.setMax(ai.maxProgress); 258 vh.progress.setProgress(ai.progress); 259 } else { 260 vh.progress.setVisibility(View.GONE); 261 vh.errorReport.setVisibility(View.VISIBLE); 262 vh.errorReport.setText(ai.errorMessage); 263 } 264 } 265 } 266 267 /** 268 * Handler for updating UI from async workers 269 * 270 * TODO: I don't know the right paradigm for updating a progress bar in a ListView. I'd 271 * like to be able to say, "update it if it's visible, skip it if it's not visible." 272 */ 273 class UIHandler extends Handler { 274 private static final int MSG_SET_MAX = 1; 275 private static final int MSG_SET_PROGRESS = 2; 276 private static final int MSG_INC_PROGRESS = 3; 277 private static final int MSG_ERROR = 4; 278 279 @Override 280 public void handleMessage(android.os.Message msg) { 281 switch (msg.what) { 282 case MSG_SET_MAX: 283 mLegacyAccounts[msg.arg1].maxProgress = msg.arg2; 284 mListView.invalidateViews(); // find a less annoying way to do that 285 break; 286 case MSG_SET_PROGRESS: 287 mLegacyAccounts[msg.arg1].progress = msg.arg2; 288 mListView.invalidateViews(); // find a less annoying way to do that 289 break; 290 case MSG_INC_PROGRESS: 291 mLegacyAccounts[msg.arg1].progress += msg.arg2; 292 mListView.invalidateViews(); // find a less annoying way to do that 293 break; 294 case MSG_ERROR: 295 mLegacyAccounts[msg.arg1].errorMessage = (String) msg.obj; 296 mListView.invalidateViews(); // find a less annoying way to do that 297 mProceedButton.setEnabled(true); 298 break; 299 default: 300 super.handleMessage(msg); 301 } 302 } 303 304 public void setMaxProgress(int accountNum, int max) { 305 android.os.Message msg = android.os.Message.obtain(); 306 msg.what = MSG_SET_MAX; 307 msg.arg1 = accountNum; 308 msg.arg2 = max; 309 sendMessage(msg); 310 } 311 312 public void setProgress(int accountNum, int progress) { 313 android.os.Message msg = android.os.Message.obtain(); 314 msg.what = MSG_SET_PROGRESS; 315 msg.arg1 = accountNum; 316 msg.arg2 = progress; 317 sendMessage(msg); 318 } 319 320 public void incProgress(int accountNum) { 321 incProgress(accountNum, 1); 322 } 323 324 public void incProgress(int accountNum, int incrementBy) { 325 if (incrementBy == 0) return; 326 android.os.Message msg = android.os.Message.obtain(); 327 msg.what = MSG_INC_PROGRESS; 328 msg.arg1 = accountNum; 329 msg.arg2 = incrementBy; 330 sendMessage(msg); 331 } 332 333 // Note: also enables the "OK" button, so we pause when complete 334 public void error(int accountNum, String error) { 335 android.os.Message msg = android.os.Message.obtain(); 336 msg.what = MSG_ERROR; 337 msg.arg1 = accountNum; 338 msg.obj = error; 339 sendMessage(msg); 340 } 341 } 342 343 /** 344 * Everything above was UI plumbing. This is the meat of this class - a conversion 345 * engine to rebuild accounts from the "LocalStore" (pre Android 2.0) format to the 346 * "Provider" (2.0 and beyond) format. 347 */ 348 private class ConversionTask extends AsyncTask<Void, Void, Void> { 349 UpgradeAccounts.AccountInfo[] mAccountInfo; 350 final Context mContext; 351 final Preferences mPreferences; 352 353 public ConversionTask(UpgradeAccounts.AccountInfo[] accountInfo) { 354 // TODO: should I copy this? 355 mAccountInfo = accountInfo; 356 mContext = UpgradeAccounts.this; 357 mPreferences = Preferences.getPreferences(mContext); 358 } 359 360 @Override 361 protected Void doInBackground(Void... params) { 362 // Globally synchronize this entire block to prevent it from running in multiple 363 // threads. this is used in case we wind up relaunching during a conversion. 364 // If this is anything but the first thread, sConversionHasRun will be set and we'll 365 // exit immediately when it's all over. 366 synchronized (sConversionInProgress) { 367 if (sConversionHasRun) { 368 return null; 369 } 370 sConversionHasRun = true; 371 372 UIHandler handler = UpgradeAccounts.this.mHandler; 373 // Step 1: Analyze accounts and generate progress max values 374 for (int i = 0; i < mAccountInfo.length; i++) { 375 int estimate = UpgradeAccounts.estimateWork(mContext, mAccountInfo[i].account); 376 if (estimate == -1) { 377 mAccountInfo[i].isError = true; 378 mHandler.error(i, mContext.getString(R.string.upgrade_accounts_error)); 379 } 380 UpgradeAccounts.this.mHandler.setMaxProgress(i, estimate); 381 } 382 383 // Step 2: Scrub accounts, deleting anything we're not keeping to reclaim storage 384 for (int i = 0; i < mAccountInfo.length; i++) { 385 if (!mAccountInfo[i].isError) { 386 boolean ok = scrubAccount(mContext, mAccountInfo[i].account, i, handler); 387 if (!ok) { 388 mAccountInfo[i].isError = true; 389 mHandler.error(i, mContext.getString(R.string.upgrade_accounts_error)); 390 } 391 } 392 } 393 394 // Step 3: Copy accounts (and delete old accounts). POP accounts first. 395 // Note: We don't check error condition here because we still want to 396 // delete the remaining parts of all accounts (even if in error condition). 397 for (int i = 0; i < mAccountInfo.length; i++) { 398 AccountInfo info = mAccountInfo[i]; 399 copyAndDeleteAccount(info, i, handler, Store.STORE_SCHEME_POP3); 400 } 401 // IMAP accounts next. 402 for (int i = 0; i < mAccountInfo.length; i++) { 403 AccountInfo info = mAccountInfo[i]; 404 copyAndDeleteAccount(info, i, handler, Store.STORE_SCHEME_IMAP); 405 } 406 407 // Step 4: Enable app-wide features such as composer, and start mail service(s) 408 Email.setServicesEnabled(mContext); 409 } 410 411 return null; 412 } 413 414 /** 415 * Copy and delete one account (helper for doInBackground). Can select accounts by type 416 * to force conversion of one or another type only. 417 */ 418 private void copyAndDeleteAccount(AccountInfo info, int i, UIHandler handler, String type) { 419 try { 420 if (type != null) { 421 String storeUri = info.account.getStoreUri(); 422 boolean isType = storeUri.startsWith(type); 423 if (!isType) { 424 return; // skip this account 425 } 426 } 427 // Don't try copying if this account is already in error state 428 if (!info.isError) { 429 copyAccount(mContext, info.account, i, handler); 430 } 431 } catch (RuntimeException e) { 432 Log.d(Email.LOG_TAG, "Exception while copying account " + e); 433 mHandler.error(i, mContext.getString(R.string.upgrade_accounts_error)); 434 info.isError = true; 435 } 436 // best effort to delete it (whether copied or not) 437 try { 438 deleteAccountStore(mContext, info.account, i, handler); 439 info.account.delete(mPreferences); 440 } catch (RuntimeException e) { 441 Log.d(Email.LOG_TAG, "Exception while deleting account " + e); 442 // No user notification is required here - we're done 443 } 444 // jam the progress indicator to mark account "complete" (in case est was wrong) 445 handler.setProgress(i, Integer.MAX_VALUE); 446 } 447 448 @Override 449 protected void onPostExecute(Void result) { 450 if (!isCancelled()) { 451 // if there were no errors, we never enabled the OK button, but 452 // we'll just proceed through anyway and return to the Welcome activity 453 if (!mProceedButton.isEnabled()) { 454 onClickOk(); 455 } 456 } 457 } 458 } 459 460 /** 461 * Estimate the work required to convert an account. 462 * 1 (account) + # folders + # messages + # attachments 463 * @return conversion operations estimate, or -1 if there's any problem 464 */ 465 /* package */ static int estimateWork(Context context, Account account) { 466 int estimate = 1; // account 467 try { 468 LocalStore store = LocalStore.newInstance(account.getLocalStoreUri(), context, null); 469 Folder[] folders = store.getPersonalNamespaces(); 470 estimate += folders.length; 471 for (int i = 0; i < folders.length; i++) { 472 Folder folder = folders[i]; 473 folder.open(Folder.OpenMode.READ_ONLY, null); 474 estimate += folder.getMessageCount(); 475 folder.close(false); 476 } 477 estimate += store.getStoredAttachmentCount(); 478 store.close(); 479 } catch (MessagingException e) { 480 Log.d(Email.LOG_TAG, "Exception while estimating account size " + e); 481 return -1; 482 } catch (RuntimeException e) { 483 Log.d(Email.LOG_TAG, "Exception while estimating account size " + e); 484 return -1; 485 } 486 return estimate; 487 } 488 489 /** 490 * Clean out an account. 491 * 492 * For IMAP: Anything we can reload from server, we delete. This reduces the risk of running 493 * out of disk space by copying everything. 494 * For POP: Delete the trash folder (which we won't bring forward). 495 * @return true if successful, false if any kind of error 496 */ 497 /* package */ static boolean scrubAccount(Context context, Account account, int accountNum, 498 UIHandler handler) { 499 try { 500 String storeUri = account.getStoreUri(); 501 boolean isImap = storeUri.startsWith(Store.STORE_SCHEME_IMAP); 502 LocalStore store = LocalStore.newInstance(account.getLocalStoreUri(), context, null); 503 Folder[] folders = store.getPersonalNamespaces(); 504 for (Folder folder : folders) { 505 folder.open(Folder.OpenMode.READ_ONLY, null); 506 String folderName = folder.getName(); 507 if ("drafts".equalsIgnoreCase(folderName)) { 508 // do not delete drafts 509 } else if ("outbox".equalsIgnoreCase(folderName)) { 510 // do not delete outbox 511 } else if ("sent".equalsIgnoreCase(folderName)) { 512 // do not delete sent 513 } else if (isImap || "trash".equalsIgnoreCase(folderName)) { 514 Log.d(Email.LOG_TAG, "Scrub " + account.getDescription() + "." + folderName); 515 // for all other folders, delete the folder (and its messages & attachments) 516 int messageCount = folder.getMessageCount(); 517 folder.delete(true); 518 if (handler != null) { 519 handler.incProgress(accountNum, 1 + messageCount); 520 } 521 } 522 folder.close(false); 523 } 524 int pruned = store.pruneCachedAttachments(); 525 if (handler != null) { 526 handler.incProgress(accountNum, pruned); 527 } 528 store.close(); 529 } catch (MessagingException e) { 530 Log.d(Email.LOG_TAG, "Exception while scrubbing account " + e); 531 return false; 532 } catch (RuntimeException e) { 533 Log.d(Email.LOG_TAG, "Exception while scrubbing account " + e); 534 return false; 535 } 536 return true; 537 } 538 539 private static class FolderConversion { 540 final Folder folder; 541 final EmailContent.Mailbox mailbox; 542 543 public FolderConversion(Folder _folder, EmailContent.Mailbox _mailbox) { 544 folder = _folder; 545 mailbox = _mailbox; 546 } 547 } 548 549 /** 550 * Copy an account. 551 */ 552 /* package */ static void copyAccount(Context context, Account account, int accountNum, 553 UIHandler handler) { 554 // If already exists- just skip it 555 int existCount = EmailContent.count(context, EmailContent.Account.CONTENT_URI, 556 WHERE_ACCOUNT_UUID_IS, new String[] { account.getUuid() }); 557 if (existCount > 0) { 558 Log.d(Email.LOG_TAG, "No conversion, account exists: " + account.getDescription()); 559 if (handler != null) { 560 handler.error(accountNum, context.getString(R.string.upgrade_accounts_error)); 561 } 562 return; 563 } 564 // Create the new account and write it 565 EmailContent.Account newAccount = LegacyConversions.makeAccount(context, account); 566 cleanupConnections(context, newAccount, account); 567 newAccount.save(context); 568 if (handler != null) { 569 handler.incProgress(accountNum); 570 } 571 572 // copy the folders, making a set of them as we go, and recording a few that we 573 // need to process first (highest priority for saving the messages) 574 HashSet<FolderConversion> conversions = new HashSet<FolderConversion>(); 575 FolderConversion drafts = null; 576 FolderConversion outbox = null; 577 FolderConversion sent = null; 578 LocalStore store = null; 579 try { 580 store = LocalStore.newInstance(account.getLocalStoreUri(), context, null); 581 Folder[] folders = store.getPersonalNamespaces(); 582 for (Folder folder : folders) { 583 String folderName = null; 584 try { 585 folder.open(Folder.OpenMode.READ_ONLY, null); 586 folderName = folder.getName(); 587 Log.d(Email.LOG_TAG, "Copy " + account.getDescription() + "." + folderName); 588 EmailContent.Mailbox mailbox = 589 LegacyConversions.makeMailbox(context, newAccount, folder); 590 mailbox.save(context); 591 if (handler != null) { 592 handler.incProgress(accountNum); 593 } 594 folder.close(false); 595 // Now record the conversion, to come back and do the messages 596 FolderConversion conversion = new FolderConversion(folder, mailbox); 597 conversions.add(conversion); 598 switch (mailbox.mType) { 599 case Mailbox.TYPE_DRAFTS: 600 drafts = conversion; 601 break; 602 case Mailbox.TYPE_OUTBOX: 603 outbox = conversion; 604 break; 605 case Mailbox.TYPE_SENT: 606 sent = conversion; 607 break; 608 } 609 } catch (MessagingException e) { 610 // We make a best-effort attempt at each folder, so even if this one fails, 611 // we'll try to keep going. 612 Log.d(Email.LOG_TAG, "Exception copying folder " + folderName + ": " + e); 613 if (handler != null) { 614 handler.error(accountNum, 615 context.getString(R.string.upgrade_accounts_error)); 616 } 617 } 618 } 619 620 // copy the messages, starting with the most critical folders, and then doing the rest 621 // outbox & drafts are the most important, as they don't exist anywhere else. we also 622 // process local (outgoing) attachments here 623 if (outbox != null) { 624 copyMessages(context, outbox, true, newAccount, accountNum, handler); 625 conversions.remove(outbox); 626 } 627 if (drafts != null) { 628 copyMessages(context, drafts, true, newAccount, accountNum, handler); 629 conversions.remove(drafts); 630 } 631 if (sent != null) { 632 copyMessages(context, sent, true, newAccount, accountNum, handler); 633 conversions.remove(outbox); 634 } 635 // Now handle any remaining folders. For incoming folders we skip attachments, as they 636 // can be reloaded from the server. 637 for (FolderConversion conversion : conversions) { 638 copyMessages(context, conversion, false, newAccount, accountNum, handler); 639 } 640 } catch (MessagingException e) { 641 Log.d(Email.LOG_TAG, "Exception while copying folders " + e); 642 // Couldn't copy folders at all 643 if (handler != null) { 644 handler.error(accountNum, context.getString(R.string.upgrade_accounts_error)); 645 } 646 } finally { 647 if (store != null) { 648 store.close(); 649 } 650 } 651 } 652 653 /** 654 * Copy all messages in a given folder 655 * 656 * @param context a system context 657 * @param conversion a folder->mailbox conversion record 658 * @param localAttachments true if the attachments refer to local data (to be sent) 659 * @param newAccount the id of the newly-created account 660 * @param accountNum the UI list # of the account 661 * @param handler the handler for updating the UI 662 */ 663 /* package */ static void copyMessages(Context context, FolderConversion conversion, 664 boolean localAttachments, EmailContent.Account newAccount, int accountNum, 665 UIHandler handler) { 666 try { 667 boolean makeDeleteSentinels = (conversion.mailbox.mType == Mailbox.TYPE_INBOX) && 668 (newAccount.getDeletePolicy() == EmailContent.Account.DELETE_POLICY_NEVER); 669 conversion.folder.open(Folder.OpenMode.READ_ONLY, null); 670 Message[] oldMessages = conversion.folder.getMessages(null); 671 for (Message oldMessage : oldMessages) { 672 Exception e = null; 673 try { 674 // load message data from legacy Store 675 FetchProfile fp = new FetchProfile(); 676 fp.add(FetchProfile.Item.ENVELOPE); 677 fp.add(FetchProfile.Item.BODY); 678 conversion.folder.fetch(new Message[] { oldMessage }, fp, null); 679 EmailContent.Message newMessage = new EmailContent.Message(); 680 if (makeDeleteSentinels && oldMessage.isSet(Flag.DELETED)) { 681 // Special case for POP3 locally-deleted messages. 682 // Creates a local "deleted message sentinel" which hides the message 683 // Echos provider code in MessagingController.processPendingMoveToTrash() 684 newMessage.mAccountKey = newAccount.mId; 685 newMessage.mMailboxKey = conversion.mailbox.mId; 686 newMessage.mFlagLoaded = EmailContent.Message.FLAG_LOADED_DELETED; 687 newMessage.mFlagRead = true; 688 newMessage.mServerId = oldMessage.getUid(); 689 newMessage.save(context); 690 } else { 691 // Main case for converting real messages with bodies & attachments 692 // convert message (headers) 693 LegacyConversions.updateMessageFields(newMessage, oldMessage, 694 newAccount.mId, conversion.mailbox.mId); 695 // convert body (text) 696 EmailContent.Body newBody = new EmailContent.Body(); 697 ArrayList<Part> viewables = new ArrayList<Part>(); 698 ArrayList<Part> attachments = new ArrayList<Part>(); 699 MimeUtility.collectParts(oldMessage, viewables, attachments); 700 LegacyConversions.updateBodyFields(newBody, newMessage, viewables); 701 // commit changes so far so we have real id's 702 newMessage.save(context); 703 newBody.save(context); 704 // convert attachments 705 if (localAttachments) { 706 // These are references to local data, and should create records only 707 // (e.g. the content URI). No files should be created. 708 LegacyConversions.updateAttachments(context, newMessage, attachments, 709 true); 710 } 711 } 712 // done 713 if (handler != null) { 714 handler.incProgress(accountNum); 715 } 716 } catch (MessagingException me) { 717 e = me; 718 } catch (IOException ioe) { 719 e = ioe; 720 } 721 if (e != null) { 722 Log.d(Email.LOG_TAG, "Exception copying message " + oldMessage.getSubject() 723 + ": "+ e); 724 if (handler != null) { 725 handler.error(accountNum, 726 context.getString(R.string.upgrade_accounts_error)); 727 } 728 } 729 } 730 } catch (MessagingException e) { 731 // Couldn't copy folder at all 732 Log.d(Email.LOG_TAG, "Exception while copying messages in " + 733 conversion.folder.toString() + ": " + e); 734 if (handler != null) { 735 handler.error(accountNum, context.getString(R.string.upgrade_accounts_error)); 736 } 737 } 738 } 739 740 /** 741 * Delete an account 742 */ 743 /* package */ static void deleteAccountStore(Context context, Account account, int accountNum, 744 UIHandler handler) { 745 try { 746 Store store = LocalStore.newInstance(account.getLocalStoreUri(), context, null); 747 store.delete(); 748 // delete() closes the store 749 } catch (MessagingException e) { 750 Log.d(Email.LOG_TAG, "Exception while deleting account " + e); 751 if (handler != null) { 752 handler.error(accountNum, context.getString(R.string.upgrade_accounts_error)); 753 } 754 } 755 } 756 757 /** 758 * Cleanup SSL, TLS, etc for each converted account. 759 */ 760 /* package */ static void cleanupConnections(Context context, EmailContent.Account newAccount, 761 Account account) { 762 // 1. Look up provider for this email address 763 String email = newAccount.mEmailAddress; 764 int atSignPos = email.lastIndexOf('@'); 765 String domain = email.substring(atSignPos + 1); 766 Provider p = AccountSettingsUtils.findProviderForDomain(context, domain); 767 768 // 2. If provider found, just use its settings (overriding what user had) 769 // This is drastic but most reliable. Note: This also benefits from newer provider 770 // data that might be found in a vendor policy module. 771 if (p != null) { 772 // Incoming 773 try { 774 URI incomingUriTemplate = p.incomingUriTemplate; 775 String incomingUsername = newAccount.mHostAuthRecv.mLogin; 776 String incomingPassword = newAccount.mHostAuthRecv.mPassword; 777 URI incomingUri = new URI(incomingUriTemplate.getScheme(), incomingUsername + ":" 778 + incomingPassword, incomingUriTemplate.getHost(), 779 incomingUriTemplate.getPort(), incomingUriTemplate.getPath(), null, null); 780 newAccount.mHostAuthRecv.setStoreUri(incomingUri.toString()); 781 } catch (URISyntaxException e) { 782 // Ignore - just use the data we copied across (for better or worse) 783 } 784 // Outgoing 785 try { 786 URI outgoingUriTemplate = p.outgoingUriTemplate; 787 String outgoingUsername = newAccount.mHostAuthSend.mLogin; 788 String outgoingPassword = newAccount.mHostAuthSend.mPassword; 789 URI outgoingUri = new URI(outgoingUriTemplate.getScheme(), outgoingUsername + ":" 790 + outgoingPassword, outgoingUriTemplate.getHost(), 791 outgoingUriTemplate.getPort(), outgoingUriTemplate.getPath(), null, null); 792 newAccount.mHostAuthSend.setStoreUri(outgoingUri.toString()); 793 } catch (URISyntaxException e) { 794 // Ignore - just use the data we copied across (for better or worse) 795 } 796 Log.d(Email.LOG_TAG, "Rewriting connection details for " + account.getDescription()); 797 return; 798 } 799 800 // 3. Otherwise, use simple heuristics to adjust connection and attempt to keep it 801 // reliable. NOTE: These are the "legacy" ssl/tls encodings, not the ones in 802 // the current provider. 803 newAccount.mHostAuthRecv.mFlags |= HostAuth.FLAG_TRUST_ALL_CERTIFICATES; 804 String receiveUri = account.getStoreUri(); 805 if (receiveUri.contains("+ssl+")) { 806 // non-optional SSL - keep as is, with trust all 807 } else if (receiveUri.contains("+ssl")) { 808 // optional SSL - TBD 809 } else if (receiveUri.contains("+tls+")) { 810 // non-optional TLS - keep as is, with trust all 811 } else if (receiveUri.contains("+tls")) { 812 // optional TLS - TBD 813 } 814 newAccount.mHostAuthSend.mFlags |= HostAuth.FLAG_TRUST_ALL_CERTIFICATES; 815 String sendUri = account.getSenderUri(); 816 if (sendUri.contains("+ssl+")) { 817 // non-optional SSL - keep as is, with trust all 818 } else if (sendUri.contains("+ssl")) { 819 // optional SSL - TBD 820 } else if (sendUri.contains("+tls+")) { 821 // non-optional TLS - keep as is, with trust all 822 } else if (sendUri.contains("+tls")) { 823 // optional TLS - TBD 824 } 825 } 826 827 /** 828 * Bulk upgrade old accounts if exist. 829 * 830 * @return true if bulk upgrade has started. false otherwise. 831 */ 832 /* package */ static boolean doBulkUpgradeIfNecessary(Context context) { 833 if (bulkUpgradesRequired(context, Preferences.getPreferences(context))) { 834 UpgradeAccounts.actionStart(context); 835 return true; 836 } 837 return false; 838 } 839 840 /** 841 * Test for bulk upgrades and return true if necessary 842 * 843 * @return true if upgrades required (old accounts exist). false otherwise. 844 */ 845 private static boolean bulkUpgradesRequired(Context context, Preferences preferences) { 846 if (DEBUG_FORCE_UPGRADES) { 847 // build at least one fake account 848 Account fake = new Account(context); 849 fake.setDescription("Fake Account"); 850 fake.setEmail("user (at) gmail.com"); 851 fake.setName("First Last"); 852 fake.setSenderUri("smtp://user:password (at) smtp.gmail.com"); 853 fake.setStoreUri("imap://user:password (at) imap.gmail.com"); 854 fake.save(preferences); 855 return true; 856 } 857 858 // 1. Get list of legacy accounts and look for any non-backup entries 859 Account[] legacyAccounts = preferences.getAccounts(); 860 if (legacyAccounts.length == 0) { 861 return false; 862 } 863 864 // 2. Look at the first legacy account and decide what to do 865 // We only need to look at the first: If it's not a backup account, then it's a true 866 // legacy account, and there are one or more accounts needing upgrade. If it is a backup 867 // account, then we know for sure that there are no legacy accounts (backup deletes all 868 // old accounts, and indicates that "modern" code has already run on this device.) 869 if (0 != (legacyAccounts[0].getBackupFlags() & Account.BACKUP_FLAGS_IS_BACKUP)) { 870 return false; 871 } else { 872 return true; 873 } 874 } 875 } 876