Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e5025e1a authored by cketti's avatar cketti
Browse files

Use 'outbox_state' table for send error handling

parent 03f0fa9f
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -25,4 +25,5 @@ val mainModule = applicationContext {
    bean { TrustManagerFactory.createInstance(get()) }
    bean { LocalKeyStoreManager(get()) }
    bean { DefaultTrustedSocketFactory(get(), get()) as TrustedSocketFactory }
    bean { Clock.INSTANCE }
}
+33 −24
Original line number Diff line number Diff line
@@ -38,8 +38,6 @@ import com.fsck.k9.Account.DeletePolicy;
import com.fsck.k9.Account.Expunge;
import com.fsck.k9.AccountStats;
import com.fsck.k9.CoreResourceProvider;
import com.fsck.k9.controller.ControllerExtension.ControllerInternals;
import com.fsck.k9.core.BuildConfig;
import com.fsck.k9.DI;
import com.fsck.k9.K9;
import com.fsck.k9.K9.Intents;
@@ -49,6 +47,7 @@ import com.fsck.k9.backend.api.Backend;
import com.fsck.k9.backend.api.SyncConfig;
import com.fsck.k9.backend.api.SyncListener;
import com.fsck.k9.cache.EmailProviderCache;
import com.fsck.k9.controller.ControllerExtension.ControllerInternals;
import com.fsck.k9.controller.MessagingControllerCommands.PendingAppend;
import com.fsck.k9.controller.MessagingControllerCommands.PendingCommand;
import com.fsck.k9.controller.MessagingControllerCommands.PendingEmptyTrash;
@@ -57,6 +56,7 @@ import com.fsck.k9.controller.MessagingControllerCommands.PendingMarkAllAsRead;
import com.fsck.k9.controller.MessagingControllerCommands.PendingMoveOrCopy;
import com.fsck.k9.controller.MessagingControllerCommands.PendingSetFlag;
import com.fsck.k9.controller.ProgressBodyFactory.ProgressListener;
import com.fsck.k9.core.BuildConfig;
import com.fsck.k9.helper.Contacts;
import com.fsck.k9.mail.Address;
import com.fsck.k9.mail.AuthenticationFailedException;
@@ -78,6 +78,9 @@ import com.fsck.k9.mailstore.LocalFolder;
import com.fsck.k9.mailstore.LocalMessage;
import com.fsck.k9.mailstore.LocalStore;
import com.fsck.k9.mailstore.LocalStoreProvider;
import com.fsck.k9.mailstore.OutboxState;
import com.fsck.k9.mailstore.OutboxStateRepository;
import com.fsck.k9.mailstore.SendState;
import com.fsck.k9.mailstore.UnavailableStorageException;
import com.fsck.k9.notification.NotificationController;
import com.fsck.k9.power.TracingPowerManager;
@@ -123,7 +126,6 @@ public class MessagingController {

    private final BlockingQueue<Command> queuedCommands = new PriorityBlockingQueue<>();
    private final Set<MessagingListener> listeners = new CopyOnWriteArraySet<>();
    private final ConcurrentHashMap<String, AtomicInteger> sendCount = new ConcurrentHashMap<>();
    private final ConcurrentHashMap<Account, Pusher> pushers = new ConcurrentHashMap<>();
    private final ExecutorService threadPool = Executors.newCachedThreadPool();
    private final MemorizingMessagingListener memorizingMessagingListener = new MemorizingMessagingListener();
@@ -1189,17 +1191,6 @@ public class MessagingController {
            localFolder = localStore.getFolder(folderServerId);
            localFolder.open(Folder.OPEN_MODE_RW);

            // Allows for re-allowing sending of messages that could not be sent
            if (flag == Flag.FLAGGED && !newState &&
                    account.getOutboxFolder().equals(folderServerId)) {
                for (Message message : messages) {
                    String uid = message.getUid();
                    if (uid != null) {
                        sendCount.remove(uid);
                    }
                }
            }

            // Update the messages in the local store
            localFolder.setFlags(messages, Collections.singleton(flag), newState);

@@ -1456,9 +1447,14 @@ public class MessagingController {
            localFolder.open(Folder.OPEN_MODE_RW);
            localFolder.appendMessages(Collections.singletonList(message));
            LocalMessage localMessage = localFolder.getMessage(message.getUid());
            long messageId = localMessage.getDatabaseId();
            localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true);
            localMessage.setCachedDecryptedSubject(plaintextSubject);
            localFolder.close();

            OutboxStateRepository outboxStateRepository = localStore.getOutboxStateRepository();
            outboxStateRepository.initializeOutboxState(messageId);

            sendPendingMessages(account, listener);
        } catch (Exception e) {
            /*
@@ -1554,6 +1550,7 @@ public class MessagingController {
        boolean wasPermanentFailure = false;
        try {
            LocalStore localStore = localStoreProvider.getInstance(account);
            OutboxStateRepository outboxStateRepository = localStore.getOutboxStateRepository();
            localFolder = localStore.getFolder(
                    account.getOutboxFolder());
            if (!localFolder.exists()) {
@@ -1586,26 +1583,26 @@ public class MessagingController {

            for (LocalMessage message : localMessages) {
                if (message.isSet(Flag.DELETED)) {
                    //FIXME: When uploading a message to the remote Sent folder the move code creates a placeholder
                    // message in the Outbox. This code gets rid of these messages. It'd be preferable if the
                    // placeholder message was never created, though.
                    message.destroy();
                    continue;
                }
                try {
                    AtomicInteger count = new AtomicInteger(0);
                    AtomicInteger oldCount = sendCount.putIfAbsent(message.getUid(), count);
                    if (oldCount != null) {
                        count = oldCount;
                    }

                    Timber.i("Send count for message %s is %d", message.getUid(), count.get());
                    long messageId = message.getDatabaseId();
                    OutboxState outboxState = outboxStateRepository.getOutboxState(messageId);

                    if (count.incrementAndGet() > K9.MAX_SEND_ATTEMPTS) {
                        Timber.e("Send count for message %s can't be delivered after %d attempts. " +
                                "Giving up until the user restarts the device", message.getUid(), MAX_SEND_ATTEMPTS);
                    if (outboxState.getSendState() != SendState.READY) {
                        Timber.v("Skipping sending message " + message.getUid());
                        notificationController.showSendFailedNotification(account,
                                new MessagingException(message.getSubject()));
                        continue;
                    }

                    Timber.i("Send count for message %s is %d", message.getUid(),
                            outboxState.getNumberOfSendAttempts());

                    localFolder.fetch(Collections.singletonList(message), fp, null);
                    try {
                        if (message.getHeader(K9.IDENTITY_HEADER).length > 0 || message.isSet(Flag.DRAFT)) {
@@ -1614,6 +1611,7 @@ public class MessagingController {
                            continue;
                        }

                        outboxStateRepository.incrementSendAttempts(messageId);
                        message.setFlag(Flag.X_SEND_IN_PROGRESS, true);

                        Timber.i("Sending message with UID %s", message.getUid());
@@ -1626,13 +1624,17 @@ public class MessagingController {
                            l.synchronizeMailboxProgress(account, account.getSentFolder(), progress, todo);
                        }
                        moveOrDeleteSentMessage(account, localStore, localFolder, message);

                        outboxStateRepository.removeOutboxState(messageId);
                    } catch (AuthenticationFailedException e) {
                        outboxStateRepository.decrementSendAttempts(messageId);
                        lastFailure = e;
                        wasPermanentFailure = false;

                        handleAuthenticationFailure(account, false);
                        handleSendFailure(account, localStore, localFolder, message, e, wasPermanentFailure);
                    } catch (CertificateValidationException e) {
                        outboxStateRepository.decrementSendAttempts(messageId);
                        lastFailure = e;
                        wasPermanentFailure = false;

@@ -1642,6 +1644,13 @@ public class MessagingController {
                        lastFailure = e;
                        wasPermanentFailure = e.isPermanentFailure();

                        if (wasPermanentFailure) {
                            String errorMessage = e.getMessage();
                            outboxStateRepository.setSendAttemptError(messageId, errorMessage);
                        } else if (outboxState.getNumberOfSendAttempts() + 1 >= MAX_SEND_ATTEMPTS) {
                            outboxStateRepository.setSendAttemptsExceeded(messageId);
                        }

                        handleSendFailure(account, localStore, localFolder, message, e, wasPermanentFailure);
                    } catch (Exception e) {
                        lastFailure = e;
+9 −0
Original line number Diff line number Diff line
@@ -29,6 +29,7 @@ import android.text.TextUtils;

import com.fsck.k9.Account;
import com.fsck.k9.AccountStats;
import com.fsck.k9.Clock;
import com.fsck.k9.DI;
import com.fsck.k9.K9;
import com.fsck.k9.Preferences;
@@ -180,6 +181,7 @@ public class LocalStore {

    private final Account account;
    private final LockableDatabase database;
    private final OutboxStateRepository outboxStateRepository;

    static LocalStore createInstance(Account account, Context context) throws MessagingException {
        return new LocalStore(account, context);
@@ -209,6 +211,9 @@ public class LocalStore {
        database = new LockableDatabase(context, account.getUuid(), schemaDefinition);
        database.setStorageProviderId(account.getLocalStorageProviderId());
        database.open();

        Clock clock = DI.get(Clock.class);
        outboxStateRepository = new OutboxStateRepository(database, clock);
    }

    public static int getDbVersion() {
@@ -248,6 +253,10 @@ public class LocalStore {
        return Preferences.getPreferences(context);
    }

    public OutboxStateRepository getOutboxStateRepository() {
        return outboxStateRepository;
    }

    public long getSize() throws MessagingException {

        final StorageManager storageManager = StorageManager.getInstance(context);
+8 −0
Original line number Diff line number Diff line
package com.fsck.k9.mailstore

data class OutboxState(
        val sendState: SendState,
        val numberOfSendAttempts: Int,
        val sendError: String?,
        val sendErrorTimestamp: Long
)
+114 −0
Original line number Diff line number Diff line
package com.fsck.k9.mailstore

import android.content.ContentValues
import com.fsck.k9.Clock

class OutboxStateRepository(private val database: LockableDatabase, private val clock: Clock) {

    fun getOutboxState(messageId: Long): OutboxState {
        return database.execute(false) { db ->
            db.query(
                    TABLE_NAME,
                    COLUMNS,
                    "$COLUMN_MESSAGE_ID = ?",
                    arrayOf(messageId.toString()), null, null, null
            ).use { cursor ->
                if (!cursor.moveToFirst()) {
                    throw IllegalStateException("No outbox_state entry for message with id $messageId")
                }

                val sendStateString = cursor.getString(cursor.getColumnIndex(COLUMN_SEND_STATE))
                val numberOfSendAttempts = cursor.getInt(cursor.getColumnIndex(COLUMN_NUMBER_OF_SEND_ATTEMPTS))
                val sendErrorTimestamp = cursor.getLong(cursor.getColumnIndex(COLUMN_ERROR_TIMESTAMP))
                val sendErrorColumnIndex = cursor.getColumnIndex(COLUMN_ERROR)
                val sendError = if (cursor.isNull(sendErrorColumnIndex)) null else cursor.getString(sendErrorColumnIndex)

                val sendState = SendState.fromDatabaseName(sendStateString)

                OutboxState(sendState, numberOfSendAttempts, sendError, sendErrorTimestamp)
            }
        }
    }

    fun initializeOutboxState(messageId: Long) {
        database.execute(false) { db ->
            val contentValues = ContentValues().apply {
                put(COLUMN_MESSAGE_ID, messageId)
                put(COLUMN_SEND_STATE, SendState.READY.databaseName)
            }

            db.insert(TABLE_NAME, null, contentValues)
        }
    }

    fun removeOutboxState(messageId: Long) {
        database.execute(false) { db ->
            db.delete(TABLE_NAME, "$COLUMN_MESSAGE_ID = ?", arrayOf(messageId.toString()))
        }
    }

    fun incrementSendAttempts(messageId: Long) {
        database.execute(false) { db ->
            db.execSQL("UPDATE $TABLE_NAME " +
                    "SET $COLUMN_NUMBER_OF_SEND_ATTEMPTS = $COLUMN_NUMBER_OF_SEND_ATTEMPTS + 1 " +
                    "WHERE $COLUMN_MESSAGE_ID = ?",
                    arrayOf(messageId.toString())
            )
        }
    }

    fun decrementSendAttempts(messageId: Long) {
        database.execute(false) { db ->
            db.execSQL("UPDATE $TABLE_NAME " +
                    "SET $COLUMN_NUMBER_OF_SEND_ATTEMPTS = $COLUMN_NUMBER_OF_SEND_ATTEMPTS - 1 " +
                    "WHERE $COLUMN_MESSAGE_ID = ?",
                    arrayOf(messageId.toString())
            )
        }
    }

    fun setSendAttemptError(messageId: Long, errorMessage: String) {
        val sendErrorTimestamp = clock.time

        database.execute(false) { db ->
            val contentValues = ContentValues().apply {
                put(COLUMN_SEND_STATE, SendState.ERROR.databaseName)
                put(COLUMN_ERROR_TIMESTAMP, sendErrorTimestamp)
                put(COLUMN_ERROR, errorMessage)
            }

            db.update(TABLE_NAME, contentValues, "$COLUMN_MESSAGE_ID = ?", arrayOf(messageId.toString()))
        }
    }

    fun setSendAttemptsExceeded(messageId: Long) {
        val sendErrorTimestamp = clock.time

        database.execute(false) { db ->
            val contentValues = ContentValues().apply {
                put(COLUMN_SEND_STATE, SendState.RETRIES_EXCEEDED.databaseName)
                put(COLUMN_ERROR_TIMESTAMP, sendErrorTimestamp)
                putNull(COLUMN_ERROR)
            }

            db.update(TABLE_NAME, contentValues, "$COLUMN_MESSAGE_ID = ?", arrayOf(messageId.toString()))
        }
    }


    companion object {
        private const val TABLE_NAME = "outbox_state"
        private const val COLUMN_MESSAGE_ID = "message_id"
        private const val COLUMN_SEND_STATE = "send_state"
        private const val COLUMN_NUMBER_OF_SEND_ATTEMPTS = "number_of_send_attempts"
        private const val COLUMN_ERROR_TIMESTAMP = "error_timestamp"
        private const val COLUMN_ERROR = "error"

        private val COLUMNS = arrayOf(
                COLUMN_SEND_STATE,
                COLUMN_NUMBER_OF_SEND_ATTEMPTS,
                COLUMN_ERROR_TIMESTAMP,
                COLUMN_ERROR
        )
    }
}
Loading