
package com.fsck.k9;

import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.HashMap;
import java.util.concurrent.ConcurrentHashMap;

import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Color;
import android.net.Uri;
import timber.log.Timber;

import com.fsck.k9.activity.setup.AccountSetupCheckSettings.CheckDirection;
import com.fsck.k9.helper.Utility;
import com.fsck.k9.mail.Address;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.NetworkType;
import com.fsck.k9.mail.Store;
import com.fsck.k9.mail.Folder.FolderClass;
import com.fsck.k9.mail.filter.Base64;
import com.fsck.k9.mail.store.RemoteStore;
import com.fsck.k9.mail.store.StoreConfig;
import com.fsck.k9.mailstore.StorageManager;
import com.fsck.k9.mailstore.StorageManager.StorageProvider;
import com.fsck.k9.mailstore.LocalStore;
import com.fsck.k9.preferences.StorageEditor;
import com.fsck.k9.preferences.Storage;
import com.fsck.k9.provider.EmailProvider;
import com.fsck.k9.provider.EmailProvider.StatsColumns;
import com.fsck.k9.search.ConditionsTreeNode;
import com.fsck.k9.search.LocalSearch;
import com.fsck.k9.search.SqlQueryBuilder;
import com.fsck.k9.search.SearchSpecification.Attribute;
import com.fsck.k9.search.SearchSpecification.SearchCondition;
import com.fsck.k9.search.SearchSpecification.SearchField;
import com.fsck.k9.mail.ssl.LocalKeyStore;
import com.fsck.k9.view.ColorChip;
import com.larswerkman.colorpicker.ColorPicker;

import static com.fsck.k9.Preferences.getEnumStringPref;

/**
 * Account stores all of the settings for a single account defined by the user. It is able to save
 * and delete itself given a Preferences to work with. Each account is defined by a UUID.
 */
public class Account implements BaseAccount, StoreConfig {
    /**
     * Default value for the inbox folder (never changes for POP3 and IMAP)
     */
    private static final String INBOX = "INBOX";

    /**
     * This local folder is used to store messages to be sent.
     */
    public static final String OUTBOX = "K9MAIL_INTERNAL_OUTBOX";

    public enum Expunge {
        EXPUNGE_IMMEDIATELY,
        EXPUNGE_MANUALLY,
        EXPUNGE_ON_POLL
    }

    public enum DeletePolicy {
        NEVER(0),
        SEVEN_DAYS(1),
        ON_DELETE(2),
        MARK_AS_READ(3);

        public final int setting;

        DeletePolicy(int setting) {
            this.setting = setting;
        }

        public String preferenceString() {
            return Integer.toString(setting);
        }

        public static DeletePolicy fromInt(int initialSetting) {
            for (DeletePolicy policy: values()) {
                if (policy.setting == initialSetting) {
                    return policy;
                }
            }
            throw new IllegalArgumentException("DeletePolicy " + initialSetting + " unknown");
        }
    }

    public static final MessageFormat DEFAULT_MESSAGE_FORMAT = MessageFormat.HTML;
    public static final boolean DEFAULT_MESSAGE_FORMAT_AUTO = false;
    public static final boolean DEFAULT_MESSAGE_READ_RECEIPT = false;
    public static final QuoteStyle DEFAULT_QUOTE_STYLE = QuoteStyle.PREFIX;
    public static final String DEFAULT_QUOTE_PREFIX = ">";
    public static final boolean DEFAULT_QUOTED_TEXT_SHOWN = true;
    public static final boolean DEFAULT_REPLY_AFTER_QUOTE = false;
    public static final boolean DEFAULT_STRIP_SIGNATURE = true;
    public static final int DEFAULT_REMOTE_SEARCH_NUM_RESULTS = 25;

    public static final String ACCOUNT_DESCRIPTION_KEY = "description";
    public static final String STORE_URI_KEY = "storeUri";
    public static final String TRANSPORT_URI_KEY = "transportUri";

    public static final String IDENTITY_NAME_KEY = "name";
    public static final String IDENTITY_EMAIL_KEY = "email";
    public static final String IDENTITY_DESCRIPTION_KEY = "description";

    /*
     * http://developer.android.com/design/style/color.html
     * Note: Order does matter, it's the order in which they will be picked.
     */
    private static final Integer[] PREDEFINED_COLORS = new Integer[] {
            Color.parseColor("#0099CC"),    // blue
            Color.parseColor("#669900"),    // green
            Color.parseColor("#FF8800"),    // orange
            Color.parseColor("#CC0000"),    // red
            Color.parseColor("#9933CC")     // purple
    };

    public enum SortType {
        SORT_DATE(R.string.sort_earliest_first, R.string.sort_latest_first, false),
        SORT_ARRIVAL(R.string.sort_earliest_first, R.string.sort_latest_first, false),
        SORT_SUBJECT(R.string.sort_subject_alpha, R.string.sort_subject_re_alpha, true),
        SORT_SENDER(R.string.sort_sender_alpha, R.string.sort_sender_re_alpha, true),
        SORT_UNREAD(R.string.sort_unread_first, R.string.sort_unread_last, true),
        SORT_FLAGGED(R.string.sort_flagged_first, R.string.sort_flagged_last, true),
        SORT_ATTACHMENT(R.string.sort_attach_first, R.string.sort_unattached_first, true);

        private int ascendingToast;
        private int descendingToast;
        private boolean defaultAscending;

        SortType(int ascending, int descending, boolean ndefaultAscending) {
            ascendingToast = ascending;
            descendingToast = descending;
            defaultAscending = ndefaultAscending;
        }

        public int getToast(boolean ascending) {
            return (ascending) ? ascendingToast : descendingToast;
        }

        public boolean isDefaultAscending() {
            return defaultAscending;
        }
    }

    public static final SortType DEFAULT_SORT_TYPE = SortType.SORT_DATE;
    public static final boolean DEFAULT_SORT_ASCENDING = false;
    public static final long NO_OPENPGP_KEY = 0;

    private DeletePolicy deletePolicy = DeletePolicy.NEVER;

    private final String accountUuid;
    private String storeUri;

    /**
     * Storage provider ID, used to locate and manage the underlying DB/file
     * storage
     */
    private String localStorageProviderId;
    private String transportUri;
    private String description;
    private String alwaysBcc;
    private int automaticCheckIntervalMinutes;
    private int displayCount;
    private int chipColor;
    private long latestOldMessageSeenTime;
    private boolean notifyNewMail;
    private FolderMode folderNotifyNewMailMode;
    private boolean notifySelfNewMail;
    private boolean notifyContactsMailOnly;
    private String inboxFolderName;
    private String draftsFolderName;
    private String sentFolderName;
    private String trashFolderName;
    private String archiveFolderName;
    private String spamFolderName;
    private String autoExpandFolderName;
    private FolderMode folderDisplayMode;
    private FolderMode folderSyncMode;
    private FolderMode folderPushMode;
    private FolderMode folderTargetMode;
    private int accountNumber;
    private boolean pushPollOnConnect;
    private boolean notifySync;
    private SortType sortType;
    private Map<SortType, Boolean> sortAscending = new HashMap<>();
    private ShowPictures showPictures;
    private boolean isSignatureBeforeQuotedText;
    private Expunge expungePolicy = Expunge.EXPUNGE_IMMEDIATELY;
    private int maxPushFolders;
    private int idleRefreshMinutes;
    private boolean goToUnreadMessageSearch;
    private final Map<NetworkType, Boolean> compressionMap = new ConcurrentHashMap<>();
    private Searchable searchableFolders;
    private boolean subscribedFoldersOnly;
    private int maximumPolledMessageAge;
    private int maximumAutoDownloadMessageSize;
    // Tracks if we have sent a notification for this account for
    // current set of fetched messages
    private boolean ringNotified;
    private MessageFormat messageFormat;
    private boolean messageFormatAuto;
    private boolean messageReadReceipt;
    private QuoteStyle quoteStyle;
    private String quotePrefix;
    private boolean defaultQuotedTextShown;
    private boolean replyAfterQuote;
    private boolean stripSignature;
    private boolean syncRemoteDeletions;
    private long pgpCryptoKey;
    private boolean markMessageAsReadOnView;
    private boolean alwaysShowCcBcc;
    private boolean allowRemoteSearch;
    private boolean remoteSearchFullText;
    private int remoteSearchNumResults;

    private ColorChip unreadColorChip;
    private ColorChip readColorChip;

    private ColorChip flaggedUnreadColorChip;
    private ColorChip flaggedReadColorChip;


    /**
     * Indicates whether this account is enabled, i.e. ready for use, or not.
     *
     * <p>
     * Right now newly imported accounts are disabled if the settings file didn't contain a
     * password for the incoming and/or outgoing server.
     * </p>
     */
    private boolean isEnabled;

    /**
     * Name of the folder that was last selected for a copy or move operation.
     *
     * Note: For now this value isn't persisted. So it will be reset when
     *       K-9 Mail is restarted.
     */
    private String lastSelectedFolderName = null;

    private List<Identity> identities;

    private NotificationSetting notificationSetting = new NotificationSetting();

    public enum FolderMode {
        NONE, ALL, FIRST_CLASS, FIRST_AND_SECOND_CLASS, NOT_SECOND_CLASS
    }

    public enum ShowPictures {
        NEVER, ALWAYS, ONLY_FROM_CONTACTS
    }

    public enum Searchable {
        ALL, DISPLAYABLE, NONE
    }

    public enum QuoteStyle {
        PREFIX, HEADER
    }

    public enum MessageFormat {
        TEXT, HTML, AUTO
    }

    protected Account(Context context) {
        accountUuid = UUID.randomUUID().toString();
        localStorageProviderId = StorageManager.getInstance(context).getDefaultProviderId();
        automaticCheckIntervalMinutes = -1;
        idleRefreshMinutes = 24;
        pushPollOnConnect = true;
        displayCount = K9.DEFAULT_VISIBLE_LIMIT;
        accountNumber = -1;
        notifyNewMail = true;
        folderNotifyNewMailMode = FolderMode.ALL;
        notifySync = true;
        notifySelfNewMail = true;
        notifyContactsMailOnly = false;
        folderDisplayMode = FolderMode.NOT_SECOND_CLASS;
        folderSyncMode = FolderMode.FIRST_CLASS;
        folderPushMode = FolderMode.FIRST_CLASS;
        folderTargetMode = FolderMode.NOT_SECOND_CLASS;
        sortType = DEFAULT_SORT_TYPE;
        sortAscending.put(DEFAULT_SORT_TYPE, DEFAULT_SORT_ASCENDING);
        showPictures = ShowPictures.NEVER;
        isSignatureBeforeQuotedText = false;
        expungePolicy = Expunge.EXPUNGE_IMMEDIATELY;
        autoExpandFolderName = INBOX;
        inboxFolderName = INBOX;
        maxPushFolders = 10;
        chipColor = pickColor(context);
        goToUnreadMessageSearch = false;
        subscribedFoldersOnly = false;
        maximumPolledMessageAge = -1;
        maximumAutoDownloadMessageSize = 32768;
        messageFormat = DEFAULT_MESSAGE_FORMAT;
        messageFormatAuto = DEFAULT_MESSAGE_FORMAT_AUTO;
        messageReadReceipt = DEFAULT_MESSAGE_READ_RECEIPT;
        quoteStyle = DEFAULT_QUOTE_STYLE;
        quotePrefix = DEFAULT_QUOTE_PREFIX;
        defaultQuotedTextShown = DEFAULT_QUOTED_TEXT_SHOWN;
        replyAfterQuote = DEFAULT_REPLY_AFTER_QUOTE;
        stripSignature = DEFAULT_STRIP_SIGNATURE;
        syncRemoteDeletions = true;
        pgpCryptoKey = NO_OPENPGP_KEY;
        allowRemoteSearch = false;
        remoteSearchFullText = false;
        remoteSearchNumResults = DEFAULT_REMOTE_SEARCH_NUM_RESULTS;
        isEnabled = true;
        markMessageAsReadOnView = true;
        alwaysShowCcBcc = false;

        searchableFolders = Searchable.ALL;

        identities = new ArrayList<>();

        Identity identity = new Identity();
        identity.setSignatureUse(true);
        identity.setSignature(context.getString(R.string.default_signature));
        identity.setDescription(context.getString(R.string.default_identity_description));
        identities.add(identity);

        notificationSetting = new NotificationSetting();
        notificationSetting.setVibrate(false);
        notificationSetting.setVibratePattern(0);
        notificationSetting.setVibrateTimes(5);
        notificationSetting.setRingEnabled(true);
        notificationSetting.setRingtone("content://settings/system/notification_sound");
        notificationSetting.setLedColor(chipColor);

        cacheChips();
    }

    /*
     * Pick a nice Android guidelines color if we haven't used them all yet.
     */
    private int pickColor(Context context) {
        List<Account> accounts = Preferences.getPreferences(context).getAccounts();

        List<Integer> availableColors = new ArrayList<>(PREDEFINED_COLORS.length);
        Collections.addAll(availableColors, PREDEFINED_COLORS);

        for (Account account : accounts) {
            Integer color = account.getChipColor();
            if (availableColors.contains(color)) {
                availableColors.remove(color);
                if (availableColors.isEmpty()) {
                    break;
                }
            }
        }

        return (availableColors.isEmpty()) ? ColorPicker.getRandomColor() : availableColors.get(0);
    }

    protected Account(Preferences preferences, String uuid) {
        this.accountUuid = uuid;
        loadAccount(preferences);
    }

    /**
     * Load stored settings for this account.
     */
    private synchronized void loadAccount(Preferences preferences) {

        Storage storage = preferences.getStorage();

        storeUri = Base64.decode(storage.getString(accountUuid + ".storeUri", null));
        localStorageProviderId = storage.getString(
                accountUuid + ".localStorageProvider", StorageManager.getInstance(K9.app).getDefaultProviderId());
        transportUri = Base64.decode(storage.getString(accountUuid + ".transportUri", null));
        description = storage.getString(accountUuid + ".description", null);
        alwaysBcc = storage.getString(accountUuid + ".alwaysBcc", alwaysBcc);
        automaticCheckIntervalMinutes = storage.getInt(accountUuid + ".automaticCheckIntervalMinutes", -1);
        idleRefreshMinutes = storage.getInt(accountUuid + ".idleRefreshMinutes", 24);
        pushPollOnConnect = storage.getBoolean(accountUuid + ".pushPollOnConnect", true);
        displayCount = storage.getInt(accountUuid + ".displayCount", K9.DEFAULT_VISIBLE_LIMIT);
        if (displayCount < 0) {
            displayCount = K9.DEFAULT_VISIBLE_LIMIT;
        }
        latestOldMessageSeenTime = storage.getLong(accountUuid + ".latestOldMessageSeenTime", 0);
        notifyNewMail = storage.getBoolean(accountUuid + ".notifyNewMail", false);

        folderNotifyNewMailMode = getEnumStringPref(storage, accountUuid + ".folderNotifyNewMailMode", FolderMode.ALL);
        notifySelfNewMail = storage.getBoolean(accountUuid + ".notifySelfNewMail", true);
        notifyContactsMailOnly = storage.getBoolean(accountUuid + ".notifyContactsMailOnly", false);
        notifySync = storage.getBoolean(accountUuid + ".notifyMailCheck", false);
        deletePolicy =  DeletePolicy.fromInt(storage.getInt(accountUuid + ".deletePolicy", DeletePolicy.NEVER.setting));
        inboxFolderName = storage.getString(accountUuid + ".inboxFolderName", INBOX);
        draftsFolderName = storage.getString(accountUuid + ".draftsFolderName", "Drafts");
        sentFolderName = storage.getString(accountUuid + ".sentFolderName", "Sent");
        trashFolderName = storage.getString(accountUuid + ".trashFolderName", "Trash");
        archiveFolderName = storage.getString(accountUuid + ".archiveFolderName", "Archive");
        spamFolderName = storage.getString(accountUuid + ".spamFolderName", "Spam");
        expungePolicy = getEnumStringPref(storage, accountUuid + ".expungePolicy", Expunge.EXPUNGE_IMMEDIATELY);
        syncRemoteDeletions = storage.getBoolean(accountUuid + ".syncRemoteDeletions", true);

        maxPushFolders = storage.getInt(accountUuid + ".maxPushFolders", 10);
        goToUnreadMessageSearch = storage.getBoolean(accountUuid + ".goToUnreadMessageSearch", false);
        subscribedFoldersOnly = storage.getBoolean(accountUuid + ".subscribedFoldersOnly", false);
        maximumPolledMessageAge = storage.getInt(accountUuid + ".maximumPolledMessageAge", -1);
        maximumAutoDownloadMessageSize = storage.getInt(accountUuid + ".maximumAutoDownloadMessageSize", 32768);
        messageFormat =  getEnumStringPref(storage, accountUuid + ".messageFormat", DEFAULT_MESSAGE_FORMAT);
        messageFormatAuto = storage.getBoolean(accountUuid + ".messageFormatAuto", DEFAULT_MESSAGE_FORMAT_AUTO);
        if (messageFormatAuto && messageFormat == MessageFormat.TEXT) {
            messageFormat = MessageFormat.AUTO;
        }
        messageReadReceipt = storage.getBoolean(accountUuid + ".messageReadReceipt", DEFAULT_MESSAGE_READ_RECEIPT);
        quoteStyle = getEnumStringPref(storage, accountUuid + ".quoteStyle", DEFAULT_QUOTE_STYLE);
        quotePrefix = storage.getString(accountUuid + ".quotePrefix", DEFAULT_QUOTE_PREFIX);
        defaultQuotedTextShown = storage.getBoolean(accountUuid + ".defaultQuotedTextShown", DEFAULT_QUOTED_TEXT_SHOWN);
        replyAfterQuote = storage.getBoolean(accountUuid + ".replyAfterQuote", DEFAULT_REPLY_AFTER_QUOTE);
        stripSignature = storage.getBoolean(accountUuid + ".stripSignature", DEFAULT_STRIP_SIGNATURE);
        for (NetworkType type : NetworkType.values()) {
            Boolean useCompression = storage.getBoolean(accountUuid + ".useCompression." + type,
                                     true);
            compressionMap.put(type, useCompression);
        }

        autoExpandFolderName = storage.getString(accountUuid + ".autoExpandFolderName", INBOX);

        accountNumber = storage.getInt(accountUuid + ".accountNumber", 0);

        chipColor = storage.getInt(accountUuid + ".chipColor", ColorPicker.getRandomColor());

        sortType = getEnumStringPref(storage, accountUuid + ".sortTypeEnum", SortType.SORT_DATE);

        sortAscending.put(sortType, storage.getBoolean(accountUuid + ".sortAscending", false));

        showPictures = getEnumStringPref(storage, accountUuid + ".showPicturesEnum", ShowPictures.NEVER);

        notificationSetting.setVibrate(storage.getBoolean(accountUuid + ".vibrate", false));
        notificationSetting.setVibratePattern(storage.getInt(accountUuid + ".vibratePattern", 0));
        notificationSetting.setVibrateTimes(storage.getInt(accountUuid + ".vibrateTimes", 5));
        notificationSetting.setRingEnabled(storage.getBoolean(accountUuid + ".ring", true));
        notificationSetting.setRingtone(storage.getString(accountUuid + ".ringtone",
                                         "content://settings/system/notification_sound"));
        notificationSetting.setLed(storage.getBoolean(accountUuid + ".led", true));
        notificationSetting.setLedColor(storage.getInt(accountUuid + ".ledColor", chipColor));

        folderDisplayMode = getEnumStringPref(storage, accountUuid + ".folderDisplayMode", FolderMode.NOT_SECOND_CLASS);

        folderSyncMode = getEnumStringPref(storage, accountUuid + ".folderSyncMode", FolderMode.FIRST_CLASS);

        folderPushMode = getEnumStringPref(storage, accountUuid + ".folderPushMode", FolderMode.FIRST_CLASS);

        folderTargetMode = getEnumStringPref(storage, accountUuid + ".folderTargetMode", FolderMode.NOT_SECOND_CLASS);

        searchableFolders = getEnumStringPref(storage, accountUuid + ".searchableFolders", Searchable.ALL);

        isSignatureBeforeQuotedText = storage.getBoolean(accountUuid + ".signatureBeforeQuotedText", false);
        identities = loadIdentities(storage);

        pgpCryptoKey = storage.getLong(accountUuid + ".cryptoKey", NO_OPENPGP_KEY);
        allowRemoteSearch = storage.getBoolean(accountUuid + ".allowRemoteSearch", false);
        remoteSearchFullText = storage.getBoolean(accountUuid + ".remoteSearchFullText", false);
        remoteSearchNumResults = storage.getInt(accountUuid + ".remoteSearchNumResults", DEFAULT_REMOTE_SEARCH_NUM_RESULTS);

        isEnabled = storage.getBoolean(accountUuid + ".enabled", true);
        markMessageAsReadOnView = storage.getBoolean(accountUuid + ".markMessageAsReadOnView", true);
        alwaysShowCcBcc = storage.getBoolean(accountUuid + ".alwaysShowCcBcc", false);

        cacheChips();

        // Use email address as account description if necessary
        if (description == null) {
            description = getEmail();
        }
    }

    protected synchronized void delete(Preferences preferences) {
        deleteCertificates();

        // Get the list of account UUIDs
        String[] uuids = preferences.getStorage().getString("accountUuids", "").split(",");

        // Create a list of all account UUIDs excluding this account
        List<String> newUuids = new ArrayList<>(uuids.length);
        for (String uuid : uuids) {
            if (!uuid.equals(accountUuid)) {
                newUuids.add(uuid);
            }
        }

        StorageEditor editor = preferences.getStorage().edit();

        // Only change the 'accountUuids' value if this account's UUID was listed before
        if (newUuids.size() < uuids.length) {
            String accountUuids = Utility.combine(newUuids.toArray(), ',');
            editor.putString("accountUuids", accountUuids);
        }

        editor.remove(accountUuid + ".storeUri");
        editor.remove(accountUuid + ".transportUri");
        editor.remove(accountUuid + ".description");
        editor.remove(accountUuid + ".name");
        editor.remove(accountUuid + ".email");
        editor.remove(accountUuid + ".alwaysBcc");
        editor.remove(accountUuid + ".automaticCheckIntervalMinutes");
        editor.remove(accountUuid + ".pushPollOnConnect");
        editor.remove(accountUuid + ".idleRefreshMinutes");
        editor.remove(accountUuid + ".lastAutomaticCheckTime");
        editor.remove(accountUuid + ".latestOldMessageSeenTime");
        editor.remove(accountUuid + ".notifyNewMail");
        editor.remove(accountUuid + ".notifySelfNewMail");
        editor.remove(accountUuid + ".deletePolicy");
        editor.remove(accountUuid + ".draftsFolderName");
        editor.remove(accountUuid + ".sentFolderName");
        editor.remove(accountUuid + ".trashFolderName");
        editor.remove(accountUuid + ".archiveFolderName");
        editor.remove(accountUuid + ".spamFolderName");
        editor.remove(accountUuid + ".autoExpandFolderName");
        editor.remove(accountUuid + ".accountNumber");
        editor.remove(accountUuid + ".vibrate");
        editor.remove(accountUuid + ".vibratePattern");
        editor.remove(accountUuid + ".vibrateTimes");
        editor.remove(accountUuid + ".ring");
        editor.remove(accountUuid + ".ringtone");
        editor.remove(accountUuid + ".folderDisplayMode");
        editor.remove(accountUuid + ".folderSyncMode");
        editor.remove(accountUuid + ".folderPushMode");
        editor.remove(accountUuid + ".folderTargetMode");
        editor.remove(accountUuid + ".signatureBeforeQuotedText");
        editor.remove(accountUuid + ".expungePolicy");
        editor.remove(accountUuid + ".syncRemoteDeletions");
        editor.remove(accountUuid + ".maxPushFolders");
        editor.remove(accountUuid + ".searchableFolders");
        editor.remove(accountUuid + ".chipColor");
        editor.remove(accountUuid + ".led");
        editor.remove(accountUuid + ".ledColor");
        editor.remove(accountUuid + ".goToUnreadMessageSearch");
        editor.remove(accountUuid + ".subscribedFoldersOnly");
        editor.remove(accountUuid + ".maximumPolledMessageAge");
        editor.remove(accountUuid + ".maximumAutoDownloadMessageSize");
        editor.remove(accountUuid + ".messageFormatAuto");
        editor.remove(accountUuid + ".quoteStyle");
        editor.remove(accountUuid + ".quotePrefix");
        editor.remove(accountUuid + ".sortTypeEnum");
        editor.remove(accountUuid + ".sortAscending");
        editor.remove(accountUuid + ".showPicturesEnum");
        editor.remove(accountUuid + ".replyAfterQuote");
        editor.remove(accountUuid + ".stripSignature");
        editor.remove(accountUuid + ".cryptoApp"); // this is no longer set, but cleans up legacy values
        editor.remove(accountUuid + ".cryptoAutoSignature");
        editor.remove(accountUuid + ".cryptoAutoEncrypt");
        editor.remove(accountUuid + ".cryptoApp");
        editor.remove(accountUuid + ".cryptoKey");
        editor.remove(accountUuid + ".cryptoSupportSignOnly");
        editor.remove(accountUuid + ".enabled");
        editor.remove(accountUuid + ".markMessageAsReadOnView");
        editor.remove(accountUuid + ".alwaysShowCcBcc");
        editor.remove(accountUuid + ".allowRemoteSearch");
        editor.remove(accountUuid + ".remoteSearchFullText");
        editor.remove(accountUuid + ".remoteSearchNumResults");
        editor.remove(accountUuid + ".defaultQuotedTextShown");
        editor.remove(accountUuid + ".displayCount");
        editor.remove(accountUuid + ".inboxFolderName");
        editor.remove(accountUuid + ".localStorageProvider");
        editor.remove(accountUuid + ".messageFormat");
        editor.remove(accountUuid + ".messageReadReceipt");
        editor.remove(accountUuid + ".notifyMailCheck");
        for (NetworkType type : NetworkType.values()) {
            editor.remove(accountUuid + ".useCompression." + type.name());
        }
        deleteIdentities(preferences.getStorage(), editor);
        // TODO: Remove preference settings that may exist for individual
        // folders in the account.
        editor.commit();
    }

    private static int findNewAccountNumber(List<Integer> accountNumbers) {
        int newAccountNumber = -1;
        Collections.sort(accountNumbers);
        for (int accountNumber : accountNumbers) {
            if (accountNumber > newAccountNumber + 1) {
                break;
            }
            newAccountNumber = accountNumber;
        }
        newAccountNumber++;
        return newAccountNumber;
    }

    private static List<Integer> getExistingAccountNumbers(Preferences preferences) {
        List<Account> accounts = preferences.getAccounts();
        List<Integer> accountNumbers = new ArrayList<>(accounts.size());
        for (Account a : accounts) {
            accountNumbers.add(a.getAccountNumber());
        }
        return accountNumbers;
    }
    public static int generateAccountNumber(Preferences preferences) {
        List<Integer> accountNumbers = getExistingAccountNumbers(preferences);
        return findNewAccountNumber(accountNumbers);
    }

    public void move(Preferences preferences, boolean moveUp) {
        String[] uuids = preferences.getStorage().getString("accountUuids", "").split(",");
        StorageEditor editor = preferences.getStorage().edit();
        String[] newUuids = new String[uuids.length];
        if (moveUp) {
            for (int i = 0; i < uuids.length; i++) {
                if (i > 0 && uuids[i].equals(accountUuid)) {
                    newUuids[i] = newUuids[i-1];
                    newUuids[i-1] = accountUuid;
                }
                else {
                    newUuids[i] = uuids[i];
                }
            }
        }
        else {
            for (int i = uuids.length - 1; i >= 0; i--) {
                if (i < uuids.length - 1 && uuids[i].equals(accountUuid)) {
                    newUuids[i] = newUuids[i+1];
                    newUuids[i+1] = accountUuid;
                }
                else {
                    newUuids[i] = uuids[i];
                }
            }
        }
        String accountUuids = Utility.combine(newUuids, ',');
        editor.putString("accountUuids", accountUuids);
        editor.commit();
        preferences.loadAccounts();
    }

    public synchronized void save(Preferences preferences) {
        StorageEditor editor = preferences.getStorage().edit();

        if (!preferences.getStorage().getString("accountUuids", "").contains(accountUuid)) {
            /*
             * When the account is first created we assign it a unique account number. The
             * account number will be unique to that account for the lifetime of the account.
             * So, we get all the existing account numbers, sort them ascending, loop through
             * the list and check if the number is greater than 1 + the previous number. If so
             * we use the previous number + 1 as the account number. This refills gaps.
             * accountNumber starts as -1 on a newly created account. It must be -1 for this
             * algorithm to work.
             *
             * I bet there is a much smarter way to do this. Anyone like to suggest it?
             */
            List<Account> accounts = preferences.getAccounts();
            int[] accountNumbers = new int[accounts.size()];
            for (int i = 0; i < accounts.size(); i++) {
                accountNumbers[i] = accounts.get(i).getAccountNumber();
            }
            Arrays.sort(accountNumbers);
            for (int accountNumber : accountNumbers) {
                if (accountNumber > this.accountNumber + 1) {
                    break;
                }
                this.accountNumber = accountNumber;
            }
            accountNumber++;

            String accountUuids = preferences.getStorage().getString("accountUuids", "");
            accountUuids += (accountUuids.length() != 0 ? "," : "") + accountUuid;
            editor.putString("accountUuids", accountUuids);
        }

        editor.putString(accountUuid + ".storeUri", Base64.encode(storeUri));
        editor.putString(accountUuid + ".localStorageProvider", localStorageProviderId);
        editor.putString(accountUuid + ".transportUri", Base64.encode(transportUri));
        editor.putString(accountUuid + ".description", description);
        editor.putString(accountUuid + ".alwaysBcc", alwaysBcc);
        editor.putInt(accountUuid + ".automaticCheckIntervalMinutes", automaticCheckIntervalMinutes);
        editor.putInt(accountUuid + ".idleRefreshMinutes", idleRefreshMinutes);
        editor.putBoolean(accountUuid + ".pushPollOnConnect", pushPollOnConnect);
        editor.putInt(accountUuid + ".displayCount", displayCount);
        editor.putLong(accountUuid + ".latestOldMessageSeenTime", latestOldMessageSeenTime);
        editor.putBoolean(accountUuid + ".notifyNewMail", notifyNewMail);
        editor.putString(accountUuid + ".folderNotifyNewMailMode", folderNotifyNewMailMode.name());
        editor.putBoolean(accountUuid + ".notifySelfNewMail", notifySelfNewMail);
        editor.putBoolean(accountUuid + ".notifyContactsMailOnly", notifyContactsMailOnly);
        editor.putBoolean(accountUuid + ".notifyMailCheck", notifySync);
        editor.putInt(accountUuid + ".deletePolicy", deletePolicy.setting);
        editor.putString(accountUuid + ".inboxFolderName", inboxFolderName);
        editor.putString(accountUuid + ".draftsFolderName", draftsFolderName);
        editor.putString(accountUuid + ".sentFolderName", sentFolderName);
        editor.putString(accountUuid + ".trashFolderName", trashFolderName);
        editor.putString(accountUuid + ".archiveFolderName", archiveFolderName);
        editor.putString(accountUuid + ".spamFolderName", spamFolderName);
        editor.putString(accountUuid + ".autoExpandFolderName", autoExpandFolderName);
        editor.putInt(accountUuid + ".accountNumber", accountNumber);
        editor.putString(accountUuid + ".sortTypeEnum", sortType.name());
        editor.putBoolean(accountUuid + ".sortAscending", sortAscending.get(sortType));
        editor.putString(accountUuid + ".showPicturesEnum", showPictures.name());
        editor.putString(accountUuid + ".folderDisplayMode", folderDisplayMode.name());
        editor.putString(accountUuid + ".folderSyncMode", folderSyncMode.name());
        editor.putString(accountUuid + ".folderPushMode", folderPushMode.name());
        editor.putString(accountUuid + ".folderTargetMode", folderTargetMode.name());
        editor.putBoolean(accountUuid + ".signatureBeforeQuotedText", this.isSignatureBeforeQuotedText);
        editor.putString(accountUuid + ".expungePolicy", expungePolicy.name());
        editor.putBoolean(accountUuid + ".syncRemoteDeletions", syncRemoteDeletions);
        editor.putInt(accountUuid + ".maxPushFolders", maxPushFolders);
        editor.putString(accountUuid + ".searchableFolders", searchableFolders.name());
        editor.putInt(accountUuid + ".chipColor", chipColor);
        editor.putBoolean(accountUuid + ".goToUnreadMessageSearch", goToUnreadMessageSearch);
        editor.putBoolean(accountUuid + ".subscribedFoldersOnly", subscribedFoldersOnly);
        editor.putInt(accountUuid + ".maximumPolledMessageAge", maximumPolledMessageAge);
        editor.putInt(accountUuid + ".maximumAutoDownloadMessageSize", maximumAutoDownloadMessageSize);
        if (MessageFormat.AUTO.equals(messageFormat)) {
            // saving MessageFormat.AUTO as is to the database will cause downgrades to crash on
            // startup, so we save as MessageFormat.TEXT instead with a separate flag for auto.
            editor.putString(accountUuid + ".messageFormat", Account.MessageFormat.TEXT.name());
            messageFormatAuto = true;
        } else {
            editor.putString(accountUuid + ".messageFormat", messageFormat.name());
            messageFormatAuto = false;
        }
        editor.putBoolean(accountUuid + ".messageFormatAuto", messageFormatAuto);
        editor.putBoolean(accountUuid + ".messageReadReceipt", messageReadReceipt);
        editor.putString(accountUuid + ".quoteStyle", quoteStyle.name());
        editor.putString(accountUuid + ".quotePrefix", quotePrefix);
        editor.putBoolean(accountUuid + ".defaultQuotedTextShown", defaultQuotedTextShown);
        editor.putBoolean(accountUuid + ".replyAfterQuote", replyAfterQuote);
        editor.putBoolean(accountUuid + ".stripSignature", stripSignature);
        editor.putLong(accountUuid + ".cryptoKey", pgpCryptoKey);
        editor.putBoolean(accountUuid + ".allowRemoteSearch", allowRemoteSearch);
        editor.putBoolean(accountUuid + ".remoteSearchFullText", remoteSearchFullText);
        editor.putInt(accountUuid + ".remoteSearchNumResults", remoteSearchNumResults);
        editor.putBoolean(accountUuid + ".enabled", isEnabled);
        editor.putBoolean(accountUuid + ".markMessageAsReadOnView", markMessageAsReadOnView);
        editor.putBoolean(accountUuid + ".alwaysShowCcBcc", alwaysShowCcBcc);

        editor.putBoolean(accountUuid + ".vibrate", notificationSetting.isVibrateEnabled());
        editor.putInt(accountUuid + ".vibratePattern", notificationSetting.getVibratePattern());
        editor.putInt(accountUuid + ".vibrateTimes", notificationSetting.getVibrateTimes());
        editor.putBoolean(accountUuid + ".ring", notificationSetting.isRingEnabled());
        editor.putString(accountUuid + ".ringtone", notificationSetting.getRingtone());
        editor.putBoolean(accountUuid + ".led", notificationSetting.isLedEnabled());
        editor.putInt(accountUuid + ".ledColor", notificationSetting.getLedColor());

        for (NetworkType type : NetworkType.values()) {
            Boolean useCompression = compressionMap.get(type);
            if (useCompression != null) {
                editor.putBoolean(accountUuid + ".useCompression." + type, useCompression);
            }
        }
        saveIdentities(preferences.getStorage(), editor);

        editor.commit();

    }

    private void resetVisibleLimits() {
        try {
            getLocalStore().resetVisibleLimits(getDisplayCount());
        } catch (MessagingException e) {
            Timber.e(e, "Unable to reset visible limits");
        }

    }

    /**
     * @return <code>null</code> if not available
     * @throws MessagingException
     * @see {@link #isAvailable(Context)}
     */
    public AccountStats getStats(Context context) throws MessagingException {
        if (!isAvailable(context)) {
            return null;
        }

        AccountStats stats = new AccountStats();

        ContentResolver cr = context.getContentResolver();

        Uri uri = Uri.withAppendedPath(EmailProvider.CONTENT_URI,
                "account/" + getUuid() + "/stats");

        String[] projection = {
                StatsColumns.UNREAD_COUNT,
                StatsColumns.FLAGGED_COUNT
        };

        // Create LocalSearch instance to exclude special folders (Trash, Drafts, Spam, Outbox,
        // Sent) and limit the search to displayable folders.
        LocalSearch search = new LocalSearch();
        excludeSpecialFolders(search);
        limitToDisplayableFolders(search);

        // Use the LocalSearch instance to create a WHERE clause to query the content provider
        StringBuilder query = new StringBuilder();
        List<String> queryArgs = new ArrayList<>();
        ConditionsTreeNode conditions = search.getConditions();
        SqlQueryBuilder.buildWhereClause(this, conditions, query, queryArgs);

        String selection = query.toString();
        String[] selectionArgs = queryArgs.toArray(new String[0]);

        Cursor cursor = cr.query(uri, projection, selection, selectionArgs, null);
        try {
            if (cursor != null && cursor.moveToFirst()) {
                stats.unreadMessageCount = cursor.getInt(0);
                stats.flaggedMessageCount = cursor.getInt(1);
            }
        } finally {
            Utility.closeQuietly(cursor);
        }

        LocalStore localStore = getLocalStore();
        if (K9.measureAccounts()) {
            stats.size = localStore.getSize();
        }

        return stats;
    }

    public int getFolderUnreadCount(Context context, String folderName) throws MessagingException {
        if (!isAvailable(context)) {
            return 0;
        }

        int unreadMessageCount = 0;

        Cursor cursor = loadUnreadCountForFolder(context, folderName);
        try {
            if (cursor != null && cursor.moveToFirst()) {
                unreadMessageCount = cursor.getInt(0);
            }
        } finally {
            Utility.closeQuietly(cursor);
        }

        return unreadMessageCount;
    }

    private Cursor loadUnreadCountForFolder(Context context, String folderName) {
        ContentResolver cr = context.getContentResolver();

        Uri uri = Uri.withAppendedPath(EmailProvider.CONTENT_URI,
                "account/" + getUuid() + "/stats");

        String[] projection = {
                StatsColumns.UNREAD_COUNT,
        };

        LocalSearch search = new LocalSearch();
        search.addAllowedFolder(folderName);

        // Use the LocalSearch instance to create a WHERE clause to query the content provider
        StringBuilder query = new StringBuilder();
        List<String> queryArgs = new ArrayList<>();
        ConditionsTreeNode conditions = search.getConditions();
        SqlQueryBuilder.buildWhereClause(this, conditions, query, queryArgs);

        String selection = query.toString();
        String[] selectionArgs = queryArgs.toArray(new String[queryArgs.size()]);

        return cr.query(uri, projection, selection, selectionArgs, null);
    }


    public synchronized void setChipColor(int color) {
        chipColor = color;
        cacheChips();
    }

    private synchronized void cacheChips() {
        readColorChip = new ColorChip(chipColor, true, ColorChip.CIRCULAR);
        unreadColorChip = new ColorChip(chipColor, false, ColorChip.CIRCULAR);
        flaggedReadColorChip = new ColorChip(chipColor, true, ColorChip.STAR);
        flaggedUnreadColorChip = new ColorChip(chipColor, false, ColorChip.STAR);
    }

    public synchronized int getChipColor() {
        return chipColor;
    }


    public ColorChip generateColorChip(boolean messageRead, boolean messageFlagged) {
        ColorChip chip;

        if (messageRead) {
            if (messageFlagged) {
                chip = flaggedReadColorChip;
            } else {
                chip = readColorChip;
            }
        } else {
            if (messageFlagged) {
                chip = flaggedUnreadColorChip;
            } else {
                chip = unreadColorChip;
            }
        }

        return chip;
    }

    @Override
    public String getUuid() {
        return accountUuid;
    }

    public synchronized String getStoreUri() {
        return storeUri;
    }

    public synchronized void setStoreUri(String storeUri) {
        this.storeUri = storeUri;
    }

    public synchronized String getTransportUri() {
        return transportUri;
    }

    public synchronized void setTransportUri(String transportUri) {
        this.transportUri = transportUri;
    }

    @Override
    public synchronized String getDescription() {
        return description;
    }

    @Override
    public synchronized void setDescription(String description) {
        this.description = description;
    }

    public synchronized String getName() {
        return identities.get(0).getName();
    }

    public synchronized void setName(String name) {
        identities.get(0).setName(name);
    }

    public synchronized boolean getSignatureUse() {
        return identities.get(0).getSignatureUse();
    }

    public synchronized void setSignatureUse(boolean signatureUse) {
        identities.get(0).setSignatureUse(signatureUse);
    }

    public synchronized String getSignature() {
        return identities.get(0).getSignature();
    }

    public synchronized void setSignature(String signature) {
        identities.get(0).setSignature(signature);
    }

    @Override
    public synchronized String getEmail() {
        return identities.get(0).getEmail();
    }

    @Override
    public synchronized void setEmail(String email) {
        identities.get(0).setEmail(email);
    }

    public synchronized String getAlwaysBcc() {
        return alwaysBcc;
    }

    public synchronized void setAlwaysBcc(String alwaysBcc) {
        this.alwaysBcc = alwaysBcc;
    }

    /* Have we sent a new mail notification on this account */
    public boolean isRingNotified() {
        return ringNotified;
    }

    public void setRingNotified(boolean ringNotified) {
        this.ringNotified = ringNotified;
    }

    public String getLocalStorageProviderId() {
        return localStorageProviderId;
    }

    public void setLocalStorageProviderId(String id) {

        if (!localStorageProviderId.equals(id)) {

            boolean successful = false;
            try {
                switchLocalStorage(id);
                successful = true;
            } catch (MessagingException e) {
                Timber.e(e, "Switching local storage provider from %s to %s failed.", localStorageProviderId, id);
            }

            // if migration to/from SD-card failed once, it will fail again.
            if (!successful) {
                return;
            }

            localStorageProviderId = id;
        }

    }

    /**
     * Returns -1 for never.
     */
    public synchronized int getAutomaticCheckIntervalMinutes() {
        return automaticCheckIntervalMinutes;
    }

    /**
     * @param automaticCheckIntervalMinutes or -1 for never.
     */
    public synchronized boolean setAutomaticCheckIntervalMinutes(int automaticCheckIntervalMinutes) {
        int oldInterval = this.automaticCheckIntervalMinutes;
        this.automaticCheckIntervalMinutes = automaticCheckIntervalMinutes;

        return (oldInterval != automaticCheckIntervalMinutes);
    }

    public synchronized int getDisplayCount() {
        return displayCount;
    }

    public synchronized void setDisplayCount(int displayCount) {
        if (displayCount != -1) {
            this.displayCount = displayCount;
        } else {
            this.displayCount = K9.DEFAULT_VISIBLE_LIMIT;
        }
        resetVisibleLimits();
    }

    public synchronized long getLatestOldMessageSeenTime() {
        return latestOldMessageSeenTime;
    }

    public synchronized void setLatestOldMessageSeenTime(long latestOldMessageSeenTime) {
        this.latestOldMessageSeenTime = latestOldMessageSeenTime;
    }

    public synchronized boolean isNotifyNewMail() {
        return notifyNewMail;
    }

    public synchronized void setNotifyNewMail(boolean notifyNewMail) {
        this.notifyNewMail = notifyNewMail;
    }

    public synchronized FolderMode getFolderNotifyNewMailMode() {
        return folderNotifyNewMailMode;
    }

    public synchronized void setFolderNotifyNewMailMode(FolderMode folderNotifyNewMailMode) {
        this.folderNotifyNewMailMode = folderNotifyNewMailMode;
    }

    public synchronized DeletePolicy getDeletePolicy() {
        return deletePolicy;
    }

    public synchronized void setDeletePolicy(DeletePolicy deletePolicy) {
        this.deletePolicy = deletePolicy;
    }

    public boolean isSpecialFolder(String folderName) {
        return (folderName != null && (folderName.equalsIgnoreCase(getInboxFolderName()) ||
                folderName.equals(getTrashFolderName()) ||
                folderName.equals(getDraftsFolderName()) ||
                folderName.equals(getArchiveFolderName()) ||
                folderName.equals(getSpamFolderName()) ||
                folderName.equals(getOutboxFolderName()) ||
                folderName.equals(getSentFolderName())));
    }

    public synchronized String getDraftsFolderName() {
        return draftsFolderName;
    }

    public synchronized void setDraftsFolderName(String name) {
        draftsFolderName = name;
    }

    /**
     * Checks if this account has a drafts folder set.
     * @return true if account has a drafts folder set.
     */
    public synchronized boolean hasDraftsFolder() {
        return !K9.FOLDER_NONE.equalsIgnoreCase(draftsFolderName);
    }

    public synchronized String getSentFolderName() {
        return sentFolderName;
    }

    public synchronized void setSentFolderName(String name) {
        sentFolderName = name;
    }

    /**
     * Checks if this account has a sent folder set.
     * @return true if account has a sent folder set.
     */
    public synchronized boolean hasSentFolder() {
        return !K9.FOLDER_NONE.equalsIgnoreCase(sentFolderName);
    }


    public synchronized String getTrashFolderName() {
        return trashFolderName;
    }

    public synchronized void setTrashFolderName(String name) {
        trashFolderName = name;
    }

    /**
     * Checks if this account has a trash folder set.
     * @return true if account has a trash folder set.
     */
    public synchronized boolean hasTrashFolder() {
        return !K9.FOLDER_NONE.equalsIgnoreCase(trashFolderName);
    }

    public synchronized String getArchiveFolderName() {
        return archiveFolderName;
    }

    public synchronized void setArchiveFolderName(String archiveFolderName) {
        this.archiveFolderName = archiveFolderName;
    }

    /**
     * Checks if this account has an archive folder set.
     * @return true if account has an archive folder set.
     */
    public synchronized boolean hasArchiveFolder() {
        return !K9.FOLDER_NONE.equalsIgnoreCase(archiveFolderName);
    }

    public synchronized String getSpamFolderName() {
        return spamFolderName;
    }

    public synchronized void setSpamFolderName(String name) {
        spamFolderName = name;
    }

    /**
     * Checks if this account has a spam folder set.
     * @return true if account has a spam folder set.
     */
    public synchronized boolean hasSpamFolder() {
        return !K9.FOLDER_NONE.equalsIgnoreCase(spamFolderName);
    }

    public synchronized String getOutboxFolderName() {
        return OUTBOX;
    }

    public synchronized String getAutoExpandFolderName() {
        return autoExpandFolderName;
    }

    public synchronized void setAutoExpandFolderName(String name) {
        autoExpandFolderName = name;
    }

    public synchronized int getAccountNumber() {
        return accountNumber;
    }

    public synchronized FolderMode getFolderDisplayMode() {
        return folderDisplayMode;
    }

    public synchronized boolean setFolderDisplayMode(FolderMode displayMode) {
        FolderMode oldDisplayMode = folderDisplayMode;
        folderDisplayMode = displayMode;
        return oldDisplayMode != displayMode;
    }

    public synchronized FolderMode getFolderSyncMode() {
        return folderSyncMode;
    }

    public synchronized boolean setFolderSyncMode(FolderMode syncMode) {
        FolderMode oldSyncMode = folderSyncMode;
        folderSyncMode = syncMode;

        if (syncMode == FolderMode.NONE && oldSyncMode != FolderMode.NONE) {
            return true;
        }
        if (syncMode != FolderMode.NONE && oldSyncMode == FolderMode.NONE) {
            return true;
        }
        return false;
    }

    public synchronized FolderMode getFolderPushMode() {
        return folderPushMode;
    }

    public synchronized boolean setFolderPushMode(FolderMode pushMode) {
        FolderMode oldPushMode = folderPushMode;

        folderPushMode = pushMode;
        return pushMode != oldPushMode;
    }

    public synchronized boolean isShowOngoing() {
        return notifySync;
    }

    public synchronized void setShowOngoing(boolean showOngoing) {
        this.notifySync = showOngoing;
    }

    public synchronized SortType getSortType() {
        return sortType;
    }

    public synchronized void setSortType(SortType sortType) {
        this.sortType = sortType;
    }

    public synchronized boolean isSortAscending(SortType sortType) {
        if (sortAscending.get(sortType) == null) {
            sortAscending.put(sortType, sortType.isDefaultAscending());
        }
        return sortAscending.get(sortType);
    }

    public synchronized void setSortAscending(SortType sortType, boolean sortAscending) {
        this.sortAscending.put(sortType, sortAscending);
    }

    public synchronized ShowPictures getShowPictures() {
        return showPictures;
    }

    public synchronized void setShowPictures(ShowPictures showPictures) {
        this.showPictures = showPictures;
    }

    public synchronized FolderMode getFolderTargetMode() {
        return folderTargetMode;
    }

    public synchronized void setFolderTargetMode(FolderMode folderTargetMode) {
        this.folderTargetMode = folderTargetMode;
    }

    public synchronized boolean isSignatureBeforeQuotedText() {
        return isSignatureBeforeQuotedText;
    }

    public synchronized void setSignatureBeforeQuotedText(boolean mIsSignatureBeforeQuotedText) {
        this.isSignatureBeforeQuotedText = mIsSignatureBeforeQuotedText;
    }

    public synchronized boolean isNotifySelfNewMail() {
        return notifySelfNewMail;
    }

    public synchronized void setNotifySelfNewMail(boolean notifySelfNewMail) {
        this.notifySelfNewMail = notifySelfNewMail;
    }

    public synchronized boolean isNotifyContactsMailOnly() {
        return notifyContactsMailOnly;
    }

    public synchronized void setNotifyContactsMailOnly(boolean notifyContactsMailOnly) {
        this.notifyContactsMailOnly = notifyContactsMailOnly;
    }

    public synchronized Expunge getExpungePolicy() {
        return expungePolicy;
    }

    public synchronized void setExpungePolicy(Expunge expungePolicy) {
        this.expungePolicy = expungePolicy;
    }

    public synchronized int getMaxPushFolders() {
        return maxPushFolders;
    }

    public synchronized boolean setMaxPushFolders(int maxPushFolders) {
        int oldMaxPushFolders = this.maxPushFolders;
        this.maxPushFolders = maxPushFolders;
        return oldMaxPushFolders != maxPushFolders;
    }

    public LocalStore getLocalStore() throws MessagingException {
        return LocalStore.getInstance(this, K9.app);
    }

    public Store getRemoteStore() throws MessagingException {
        return RemoteStore.getInstance(K9.app, this);
    }

    // It'd be great if this actually went into the store implementation
    // to get this, but that's expensive and not easily accessible
    // during initialization
    public boolean isSearchByDateCapable() {
        return (getStoreUri().startsWith("imap"));
    }


    @Override
    public synchronized String toString() {
        return description;
    }

    public synchronized void setCompression(NetworkType networkType, boolean useCompression) {
        compressionMap.put(networkType, useCompression);
    }

    public synchronized boolean useCompression(NetworkType networkType) {
        Boolean useCompression = compressionMap.get(networkType);
        if (useCompression == null) {
            return true;
        }

        return useCompression;
    }

    @Override
    public boolean equals(Object o) {
        if (o instanceof Account) {
            return ((Account)o).accountUuid.equals(accountUuid);
        }
        return super.equals(o);
    }

    @Override
    public int hashCode() {
        return accountUuid.hashCode();
    }

    private synchronized List<Identity> loadIdentities(Storage storage) {
        List<Identity> newIdentities = new ArrayList<>();
        int ident = 0;
        boolean gotOne;
        do {
            gotOne = false;
            String name = storage.getString(accountUuid + "." + IDENTITY_NAME_KEY + "." + ident, null);
            String email = storage.getString(accountUuid + "." + IDENTITY_EMAIL_KEY + "." + ident, null);
            boolean signatureUse = storage.getBoolean(accountUuid + ".signatureUse." + ident, true);
            String signature = storage.getString(accountUuid + ".signature." + ident, null);
            String description = storage.getString(accountUuid + "." + IDENTITY_DESCRIPTION_KEY + "." + ident, null);
            final String replyTo = storage.getString(accountUuid + ".replyTo." + ident, null);
            if (email != null) {
                Identity identity = new Identity();
                identity.setName(name);
                identity.setEmail(email);
                identity.setSignatureUse(signatureUse);
                identity.setSignature(signature);
                identity.setDescription(description);
                identity.setReplyTo(replyTo);
                newIdentities.add(identity);
                gotOne = true;
            }
            ident++;
        } while (gotOne);

        if (newIdentities.isEmpty()) {
            String name = storage.getString(accountUuid + ".name", null);
            String email = storage.getString(accountUuid + ".email", null);
            boolean signatureUse = storage.getBoolean(accountUuid + ".signatureUse", true);
            String signature = storage.getString(accountUuid + ".signature", null);
            Identity identity = new Identity();
            identity.setName(name);
            identity.setEmail(email);
            identity.setSignatureUse(signatureUse);
            identity.setSignature(signature);
            identity.setDescription(email);
            newIdentities.add(identity);
        }

        return newIdentities;
    }

    private synchronized void deleteIdentities(Storage storage, StorageEditor editor) {
        int ident = 0;
        boolean gotOne;
        do {
            gotOne = false;
            String email = storage.getString(accountUuid + "." + IDENTITY_EMAIL_KEY + "." + ident, null);
            if (email != null) {
                editor.remove(accountUuid + "." + IDENTITY_NAME_KEY + "." + ident);
                editor.remove(accountUuid + "." + IDENTITY_EMAIL_KEY + "." + ident);
                editor.remove(accountUuid + ".signatureUse." + ident);
                editor.remove(accountUuid + ".signature." + ident);
                editor.remove(accountUuid + "." + IDENTITY_DESCRIPTION_KEY + "." + ident);
                editor.remove(accountUuid + ".replyTo." + ident);
                gotOne = true;
            }
            ident++;
        } while (gotOne);
    }

    private synchronized void saveIdentities(Storage storage, StorageEditor editor) {
        deleteIdentities(storage, editor);
        int ident = 0;

        for (Identity identity : identities) {
            editor.putString(accountUuid + "." + IDENTITY_NAME_KEY + "." + ident, identity.getName());
            editor.putString(accountUuid + "." + IDENTITY_EMAIL_KEY + "." + ident, identity.getEmail());
            editor.putBoolean(accountUuid + ".signatureUse." + ident, identity.getSignatureUse());
            editor.putString(accountUuid + ".signature." + ident, identity.getSignature());
            editor.putString(accountUuid + "." + IDENTITY_DESCRIPTION_KEY + "." + ident, identity.getDescription());
            editor.putString(accountUuid + ".replyTo." + ident, identity.getReplyTo());
            ident++;
        }
    }

    public synchronized List<Identity> getIdentities() {
        return identities;
    }

    public synchronized void setIdentities(List<Identity> newIdentities) {
        identities = new ArrayList<>(newIdentities);
    }

    public synchronized Identity getIdentity(int i) {
        if (i < identities.size()) {
            return identities.get(i);
        }
        throw new IllegalArgumentException("Identity with index " + i + " not found");
    }

    public boolean isAnIdentity(Address[] addrs) {
        if (addrs == null) {
            return false;
        }
        for (Address addr : addrs) {
            if (findIdentity(addr) != null) {
                return true;
            }
        }

        return false;
    }

    public boolean isAnIdentity(Address addr) {
        return findIdentity(addr) != null;
    }

    public synchronized Identity findIdentity(Address addr) {
        for (Identity identity : identities) {
            String email = identity.getEmail();
            if (email != null && email.equalsIgnoreCase(addr.getAddress())) {
                return identity;
            }
        }
        return null;
    }

    public synchronized Searchable getSearchableFolders() {
        return searchableFolders;
    }

    public synchronized void setSearchableFolders(Searchable searchableFolders) {
        this.searchableFolders = searchableFolders;
    }

    public synchronized int getIdleRefreshMinutes() {
        return idleRefreshMinutes;
    }

    public synchronized void setIdleRefreshMinutes(int idleRefreshMinutes) {
        this.idleRefreshMinutes = idleRefreshMinutes;
    }

    public synchronized boolean isPushPollOnConnect() {
        return pushPollOnConnect;
    }

    public synchronized void setPushPollOnConnect(boolean pushPollOnConnect) {
        this.pushPollOnConnect = pushPollOnConnect;
    }

    /**
     * Are we storing out localStore on the SD-card instead of the local device
     * memory?<br/>
     * Only to be called during initial account-setup!<br/>
     * Side-effect: changes {@link #localStorageProviderId}.
     *
     * @param newStorageProviderId
     *            Never <code>null</code>.
     * @throws MessagingException
     */
    private void switchLocalStorage(final String newStorageProviderId) throws MessagingException {
        if (!localStorageProviderId.equals(newStorageProviderId)) {
            getLocalStore().switchLocalStorage(newStorageProviderId);
        }
    }

    public synchronized boolean goToUnreadMessageSearch() {
        return goToUnreadMessageSearch;
    }

    public synchronized void setGoToUnreadMessageSearch(boolean goToUnreadMessageSearch) {
        this.goToUnreadMessageSearch = goToUnreadMessageSearch;
    }

    public synchronized boolean subscribedFoldersOnly() {
        return subscribedFoldersOnly;
    }

    public synchronized void setSubscribedFoldersOnly(boolean subscribedFoldersOnly) {
        this.subscribedFoldersOnly = subscribedFoldersOnly;
    }

    public synchronized int getMaximumPolledMessageAge() {
        return maximumPolledMessageAge;
    }

    public synchronized void setMaximumPolledMessageAge(int maximumPolledMessageAge) {
        this.maximumPolledMessageAge = maximumPolledMessageAge;
    }

    public synchronized int getMaximumAutoDownloadMessageSize() {
        return maximumAutoDownloadMessageSize;
    }

    public synchronized void setMaximumAutoDownloadMessageSize(int maximumAutoDownloadMessageSize) {
        this.maximumAutoDownloadMessageSize = maximumAutoDownloadMessageSize;
    }

    public Date getEarliestPollDate() {
        int age = getMaximumPolledMessageAge();
        if (age >= 0) {
            Calendar now = Calendar.getInstance();
            now.set(Calendar.HOUR_OF_DAY, 0);
            now.set(Calendar.MINUTE, 0);
            now.set(Calendar.SECOND, 0);
            now.set(Calendar.MILLISECOND, 0);
            if (age < 28) {
                now.add(Calendar.DATE, age * -1);
            } else switch (age) {
                case 28:
                    now.add(Calendar.MONTH, -1);
                    break;
                case 56:
                    now.add(Calendar.MONTH, -2);
                    break;
                case 84:
                    now.add(Calendar.MONTH, -3);
                    break;
                case 168:
                    now.add(Calendar.MONTH, -6);
                    break;
                case 365:
                    now.add(Calendar.YEAR, -1);
                    break;
                }

            return now.getTime();
        }

        return null;
    }

    public MessageFormat getMessageFormat() {
        return messageFormat;
    }

    public void setMessageFormat(MessageFormat messageFormat) {
        this.messageFormat = messageFormat;
    }

    public synchronized boolean isMessageReadReceiptAlways() {
        return messageReadReceipt;
    }

    public synchronized void setMessageReadReceipt(boolean messageReadReceipt) {
        this.messageReadReceipt = messageReadReceipt;
    }

    public QuoteStyle getQuoteStyle() {
        return quoteStyle;
    }

    public void setQuoteStyle(QuoteStyle quoteStyle) {
        this.quoteStyle = quoteStyle;
    }

    public synchronized String getQuotePrefix() {
        return quotePrefix;
    }

    public synchronized void setQuotePrefix(String quotePrefix) {
        this.quotePrefix = quotePrefix;
    }

    public synchronized boolean isDefaultQuotedTextShown() {
        return defaultQuotedTextShown;
    }

    public synchronized void setDefaultQuotedTextShown(boolean shown) {
        defaultQuotedTextShown = shown;
    }

    public synchronized boolean isReplyAfterQuote() {
        return replyAfterQuote;
    }

    public synchronized void setReplyAfterQuote(boolean replyAfterQuote) {
        this.replyAfterQuote = replyAfterQuote;
    }

    public synchronized boolean isStripSignature() {
        return stripSignature;
    }

    public synchronized void setStripSignature(boolean stripSignature) {
        this.stripSignature = stripSignature;
    }

    public long getCryptoKey() {
        return pgpCryptoKey;
    }

    public void setCryptoKey(long keyId) {
        pgpCryptoKey = keyId;
    }

    public boolean allowRemoteSearch() {
        return allowRemoteSearch;
    }

    public void setAllowRemoteSearch(boolean val) {
        allowRemoteSearch = val;
    }

    public int getRemoteSearchNumResults() {
        return remoteSearchNumResults;
    }

    public void setRemoteSearchNumResults(int val) {
        remoteSearchNumResults = (val >= 0 ? val : 0);
    }

    public String getInboxFolderName() {
        return inboxFolderName;
    }

    public void setInboxFolderName(String name) {
        this.inboxFolderName = name;
    }

    public synchronized boolean syncRemoteDeletions() {
        return syncRemoteDeletions;
    }

    public synchronized void setSyncRemoteDeletions(boolean syncRemoteDeletions) {
        this.syncRemoteDeletions = syncRemoteDeletions;
    }

    public synchronized String getLastSelectedFolderName() {
        return lastSelectedFolderName;
    }

    public synchronized void setLastSelectedFolderName(String folderName) {
        lastSelectedFolderName = folderName;
    }

    public synchronized NotificationSetting getNotificationSetting() {
        return notificationSetting;
    }

    /**
     * @return <code>true</code> if our {@link StorageProvider} is ready. (e.g.
     *         card inserted)
     */
    public boolean isAvailable(Context context) {
        String localStorageProviderId = getLocalStorageProviderId();
        boolean storageProviderIsInternalMemory = localStorageProviderId == null;
        return storageProviderIsInternalMemory || StorageManager.getInstance(context).isReady(localStorageProviderId);
    }

    public synchronized boolean isEnabled() {
        return isEnabled;
    }

    public synchronized void setEnabled(boolean enabled) {
        isEnabled = enabled;
    }

    public synchronized boolean isMarkMessageAsReadOnView() {
        return markMessageAsReadOnView;
    }

    public synchronized void setMarkMessageAsReadOnView(boolean value) {
        markMessageAsReadOnView = value;
    }

    public synchronized boolean isAlwaysShowCcBcc() {
        return alwaysShowCcBcc;
    }

    public synchronized void setAlwaysShowCcBcc(boolean show) {
        alwaysShowCcBcc = show;
    }
    public boolean isRemoteSearchFullText() {
        return false;   // Temporarily disabled
        //return remoteSearchFullText;
    }

    public void setRemoteSearchFullText(boolean val) {
        remoteSearchFullText = val;
    }

    /**
     * Modify the supplied {@link LocalSearch} instance to limit the search to displayable folders.
     *
     * <p>
     * This method uses the current folder display mode to decide what folders to include/exclude.
     * </p>
     *
     * @param search
     *         The {@code LocalSearch} instance to modify.
     *
     * @see #getFolderDisplayMode()
     */
    public void limitToDisplayableFolders(LocalSearch search) {
        final Account.FolderMode displayMode = getFolderDisplayMode();

        switch (displayMode) {
            case FIRST_CLASS: {
                // Count messages in the INBOX and non-special first class folders
                search.and(SearchField.DISPLAY_CLASS, FolderClass.FIRST_CLASS.name(),
                        Attribute.EQUALS);
                break;
            }
            case FIRST_AND_SECOND_CLASS: {
                // Count messages in the INBOX and non-special first and second class folders
                search.and(SearchField.DISPLAY_CLASS, FolderClass.FIRST_CLASS.name(),
                        Attribute.EQUALS);

                // TODO: Create a proper interface for creating arbitrary condition trees
                SearchCondition searchCondition = new SearchCondition(SearchField.DISPLAY_CLASS,
                        Attribute.EQUALS, FolderClass.SECOND_CLASS.name());
                ConditionsTreeNode root = search.getConditions();
                if (root.mRight != null) {
                    root.mRight.or(searchCondition);
                } else {
                    search.or(searchCondition);
                }
                break;
            }
            case NOT_SECOND_CLASS: {
                // Count messages in the INBOX and non-special non-second-class folders
                search.and(SearchField.DISPLAY_CLASS, FolderClass.SECOND_CLASS.name(),
                        Attribute.NOT_EQUALS);
                break;
            }
            default:
            case ALL: {
                // Count messages in the INBOX and non-special folders
                break;
            }
        }
    }

    /**
     * Modify the supplied {@link LocalSearch} instance to exclude special folders.
     *
     * <p>
     * Currently the following folders are excluded:
     * <ul>
     *   <li>Trash</li>
     *   <li>Drafts</li>
     *   <li>Spam</li>
     *   <li>Outbox</li>
     *   <li>Sent</li>
     * </ul>
     * The Inbox will always be included even if one of the special folders is configured to point
     * to the Inbox.
     * </p>
     *
     * @param search
     *         The {@code LocalSearch} instance to modify.
     */
    public void excludeSpecialFolders(LocalSearch search) {
        excludeSpecialFolder(search, getTrashFolderName());
        excludeSpecialFolder(search, getDraftsFolderName());
        excludeSpecialFolder(search, getSpamFolderName());
        excludeSpecialFolder(search, getOutboxFolderName());
        excludeSpecialFolder(search, getSentFolderName());
        search.or(new SearchCondition(SearchField.FOLDER, Attribute.EQUALS, getInboxFolderName()));
    }

    /**
     * Modify the supplied {@link LocalSearch} instance to exclude "unwanted" folders.
     *
     * <p>
     * Currently the following folders are excluded:
     * <ul>
     *   <li>Trash</li>
     *   <li>Spam</li>
     *   <li>Outbox</li>
     * </ul>
     * The Inbox will always be included even if one of the special folders is configured to point
     * to the Inbox.
     * </p>
     *
     * @param search
     *         The {@code LocalSearch} instance to modify.
     */
    public void excludeUnwantedFolders(LocalSearch search) {
        excludeSpecialFolder(search, getTrashFolderName());
        excludeSpecialFolder(search, getSpamFolderName());
        excludeSpecialFolder(search, getOutboxFolderName());
        search.or(new SearchCondition(SearchField.FOLDER, Attribute.EQUALS, getInboxFolderName()));
    }

    private void excludeSpecialFolder(LocalSearch search, String folderName) {
        if (folderName != null && !K9.FOLDER_NONE.equals(folderName)) {
            search.and(SearchField.FOLDER, folderName, Attribute.NOT_EQUALS);
        }
    }

    /**
     * Add a new certificate for the incoming or outgoing server to the local key store.
     */
    public void addCertificate(CheckDirection direction, X509Certificate certificate) throws CertificateException {
        Uri uri;
        if (direction == CheckDirection.INCOMING) {
            uri = Uri.parse(getStoreUri());
        } else {
            uri = Uri.parse(getTransportUri());
        }
        LocalKeyStore localKeyStore = LocalKeyStore.getInstance();
        localKeyStore.addCertificate(uri.getHost(), uri.getPort(), certificate);
    }

    /**
     * Examine the existing settings for an account.  If the old host/port is different from the
     * new host/port, then try and delete any (possibly non-existent) certificate stored for the
     * old host/port.
     */
    public void deleteCertificate(String newHost, int newPort, CheckDirection direction) {
        Uri uri;
        if (direction == CheckDirection.INCOMING) {
            uri = Uri.parse(getStoreUri());
        } else {
            uri = Uri.parse(getTransportUri());
        }
        String oldHost = uri.getHost();
        int oldPort = uri.getPort();
        if (oldPort == -1) {
            // This occurs when a new account is created
            return;
        }
        if (!newHost.equals(oldHost) || newPort != oldPort) {
            LocalKeyStore localKeyStore = LocalKeyStore.getInstance();
            localKeyStore.deleteCertificate(oldHost, oldPort);
        }
    }

    /**
     * Examine the settings for the account and attempt to delete (possibly non-existent)
     * certificates for the incoming and outgoing servers.
     */
    private void deleteCertificates() {
        LocalKeyStore localKeyStore = LocalKeyStore.getInstance();

        String storeUri = getStoreUri();
        if (storeUri != null) {
            Uri uri = Uri.parse(storeUri);
            localKeyStore.deleteCertificate(uri.getHost(), uri.getPort());
        }
        String transportUri = getTransportUri();
        if (transportUri != null) {
            Uri uri = Uri.parse(transportUri);
            localKeyStore.deleteCertificate(uri.getHost(), uri.getPort());
        }
    }
}
