Merge main into feat-326-logging-authlock
This commit is contained in:
@@ -2,7 +2,6 @@
|
||||
package org.libremail.data.sync
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.hilt.work.HiltWorker
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
@@ -24,6 +23,8 @@ import org.libremail.mail.GraphSendException
|
||||
import org.libremail.mail.GraphSender
|
||||
import org.libremail.mail.SendableAttachment
|
||||
import org.libremail.mail.SmtpSender
|
||||
import org.libremail.reporting.AppLog
|
||||
import org.libremail.reporting.accountLogRef
|
||||
import java.io.File
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
@@ -57,66 +58,89 @@ class SendWorker @AssistedInject constructor(
|
||||
val attachmentUriGrants = this.attachmentUriGrants.get()
|
||||
val pending = outboxDao.getAll()
|
||||
if (pending.isEmpty()) return Result.success()
|
||||
AppLog.i(TAG, "outbox drain: ${pending.size} queued")
|
||||
|
||||
var anyFailed = false
|
||||
for (entity in pending) {
|
||||
val attachmentDir = File(applicationContext.cacheDir, "outbox/${entity.id}")
|
||||
val account = accountDao.getById(entity.accountId)?.toDomain()
|
||||
if (account == null) {
|
||||
outboxDao.delete(entity.id) // account removed — drop the queued message
|
||||
attachmentDir.deleteRecursively()
|
||||
attachmentUriGrants.releaseUnreferenced(entity.attachmentUris())
|
||||
continue
|
||||
}
|
||||
runCatching {
|
||||
val message = OutgoingMessage(
|
||||
accountId = entity.accountId,
|
||||
to = entity.toAddresses,
|
||||
cc = entity.ccAddresses,
|
||||
bcc = entity.bccAddresses,
|
||||
subject = entity.subject,
|
||||
body = entity.body,
|
||||
bodyHtml = entity.bodyHtml,
|
||||
)
|
||||
val attachments = stagedAttachments(attachmentDir, entity.attachments.toOutgoingAttachments())
|
||||
if (account.authType == AuthType.OAUTH_OUTLOOK) {
|
||||
sendOutlook(connectionFactory, account, message, attachments)
|
||||
} else {
|
||||
smtpSender.send(
|
||||
connectionFactory.smtpParamsFor(account),
|
||||
from = account.email,
|
||||
message = message,
|
||||
attachments = attachments,
|
||||
)
|
||||
}
|
||||
}.fold(
|
||||
onSuccess = {
|
||||
outboxDao.delete(entity.id)
|
||||
attachmentDir.deleteRecursively()
|
||||
// The picked bytes were staged at enqueue; with the row sent, drop the persistable
|
||||
// grant unless a live draft/outbox row still references the same URI (security review).
|
||||
attachmentUriGrants.releaseUnreferenced(entity.attachmentUris())
|
||||
},
|
||||
onFailure = { e ->
|
||||
if (e is GraphSendException && e.mayHaveSent) {
|
||||
// Graph may already have delivered this; auto-retrying (or any other send)
|
||||
// would duplicate it, so leave it queued with a clear status and let the
|
||||
// user decide. Not counted as a failure, so WorkManager won't auto-retry.
|
||||
outboxDao.setError(
|
||||
entity.id,
|
||||
"Send status unknown — check your Sent folder, then retry or cancel",
|
||||
)
|
||||
} else {
|
||||
outboxDao.setError(entity.id, e.message)
|
||||
anyFailed = true
|
||||
}
|
||||
},
|
||||
)
|
||||
val failed = sendQueued(entity, outboxDao, accountDao, connectionFactory, attachmentUriGrants)
|
||||
if (failed) anyFailed = true
|
||||
}
|
||||
// Retry (with WorkManager backoff) so failed sends are reattempted when conditions improve.
|
||||
return if (anyFailed) Result.retry() else Result.success()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends one queued [entity] — or drops it if its account was removed — updating the outbox row
|
||||
* and releasing its staged attachment grant. Returns true if the send genuinely failed (should
|
||||
* count toward a WorkManager retry); the ambiguous "may have sent" Graph case returns false, since
|
||||
* it is deliberately left queued rather than retried (see [sendOutlook]).
|
||||
*/
|
||||
private suspend fun sendQueued(
|
||||
entity: OutboxEntity,
|
||||
outboxDao: OutboxDao,
|
||||
accountDao: AccountDao,
|
||||
connectionFactory: MailConnectionFactory,
|
||||
attachmentUriGrants: AttachmentUriGrants,
|
||||
): Boolean {
|
||||
val attachmentDir = File(applicationContext.cacheDir, "outbox/${entity.id}")
|
||||
val account = accountDao.getById(entity.accountId)?.toDomain()
|
||||
if (account == null) {
|
||||
outboxDao.delete(entity.id) // account removed — drop the queued message
|
||||
attachmentDir.deleteRecursively()
|
||||
attachmentUriGrants.releaseUnreferenced(entity.attachmentUris())
|
||||
return false
|
||||
}
|
||||
var failed = false
|
||||
runCatching {
|
||||
val message = OutgoingMessage(
|
||||
accountId = entity.accountId,
|
||||
to = entity.toAddresses,
|
||||
cc = entity.ccAddresses,
|
||||
bcc = entity.bccAddresses,
|
||||
subject = entity.subject,
|
||||
body = entity.body,
|
||||
bodyHtml = entity.bodyHtml,
|
||||
)
|
||||
val attachments = stagedAttachments(attachmentDir, entity.attachments.toOutgoingAttachments())
|
||||
if (account.authType == AuthType.OAUTH_OUTLOOK) {
|
||||
sendOutlook(connectionFactory, account, message, attachments)
|
||||
} else {
|
||||
smtpSender.send(
|
||||
connectionFactory.smtpParamsFor(account),
|
||||
from = account.email,
|
||||
message = message,
|
||||
attachments = attachments,
|
||||
)
|
||||
}
|
||||
}.fold(
|
||||
onSuccess = {
|
||||
outboxDao.delete(entity.id)
|
||||
attachmentDir.deleteRecursively()
|
||||
// The picked bytes were staged at enqueue; with the row sent, drop the persistable
|
||||
// grant unless a live draft/outbox row still references the same URI (security review).
|
||||
attachmentUriGrants.releaseUnreferenced(entity.attachmentUris())
|
||||
val via = if (account.authType == AuthType.OAUTH_OUTLOOK) "Graph" else "SMTP"
|
||||
AppLog.i(TAG, "sent ${accountLogRef(account.id)} via $via")
|
||||
},
|
||||
onFailure = { e ->
|
||||
if (e is GraphSendException && e.mayHaveSent) {
|
||||
// Graph may already have delivered this; auto-retrying (or any other send) would
|
||||
// duplicate it, so leave it queued with a clear status and let the user decide.
|
||||
// Not counted as a failure, so WorkManager won't auto-retry.
|
||||
outboxDao.setError(
|
||||
entity.id,
|
||||
"Send status unknown — check your Sent folder, then retry or cancel",
|
||||
)
|
||||
} else {
|
||||
outboxDao.setError(entity.id, e.message)
|
||||
failed = true
|
||||
AppLog.w(TAG, "send failed for ${accountLogRef(account.id)}; will retry")
|
||||
}
|
||||
},
|
||||
)
|
||||
return failed
|
||||
}
|
||||
|
||||
/**
|
||||
* Outlook prefers Microsoft Graph. Fall back to SMTP only when Graph definitely did NOT send
|
||||
* (a rejection, a pre-send/transport error, or a token failure); never fall back when the Graph
|
||||
@@ -143,7 +167,7 @@ class SendWorker @AssistedInject constructor(
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
// Graph was never reached (e.g. token refresh failed) — SMTP cannot duplicate it.
|
||||
Log.w(TAG, "Graph send failed for ${account.email}; falling back to SMTP", e)
|
||||
AppLog.w(TAG, "Graph send failed for ${accountLogRef(account.id)}; falling back to SMTP", e)
|
||||
smtpSender.send(
|
||||
connectionFactory.smtpParamsFor(account),
|
||||
from = account.email,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
package org.libremail.mail
|
||||
|
||||
import android.util.Log
|
||||
import jakarta.mail.FetchProfile
|
||||
import jakarta.mail.Flags
|
||||
import jakarta.mail.Folder
|
||||
@@ -33,6 +32,7 @@ import org.eclipse.angus.mail.imap.IMAPFolder
|
||||
import org.eclipse.angus.mail.imap.IMAPMessage
|
||||
import org.libremail.domain.model.ImapConnectionParams
|
||||
import org.libremail.domain.model.MailSecurity
|
||||
import org.libremail.reporting.AppLog
|
||||
import java.util.Properties
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
@@ -474,12 +474,12 @@ class ImapClient(private val reuseConnections: Boolean) {
|
||||
runCatching { store.close() }
|
||||
throw e
|
||||
}
|
||||
Log.d(TAG, "IDLE connected")
|
||||
AppLog.d(TAG, "IDLE connected")
|
||||
|
||||
val pushes = Channel<Unit>(Channel.CONFLATED)
|
||||
inbox.addMessageCountListener(object : MessageCountAdapter() {
|
||||
override fun messagesAdded(event: MessageCountEvent) {
|
||||
Log.d(TAG, "IDLE push: ${event.messages.size} new message(s)")
|
||||
AppLog.d(TAG, "IDLE push: ${event.messages.size} new message(s)")
|
||||
pushes.trySend(Unit)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -8,7 +8,6 @@ import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
@@ -38,6 +37,7 @@ import org.libremail.domain.model.Account
|
||||
import org.libremail.mail.ImapClient
|
||||
import org.libremail.power.BatteryStatusProvider
|
||||
import org.libremail.reporting.AppLog
|
||||
import org.libremail.reporting.accountLogRef
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
@@ -91,7 +91,7 @@ class IdleService : Service() {
|
||||
// Can't open the encrypted DB without the user present. Defer (stop) and let the app
|
||||
// restart push after the next unlock, rather than block the service and ANR.
|
||||
if (cacheGuard.isCacheLocked()) {
|
||||
Log.i(TAG, "encrypted cache locked; deferring IDLE push until the app is unlocked")
|
||||
AppLog.i(TAG, "encrypted cache locked; deferring IDLE push until the app is unlocked")
|
||||
stopSelf()
|
||||
return@launch
|
||||
}
|
||||
@@ -200,6 +200,7 @@ class IdleService : Service() {
|
||||
* idle()'s on-connect sync, so no mail is missed across renewals.
|
||||
*/
|
||||
private suspend fun watchAccount(account: Account) {
|
||||
AppLog.i(TAG, "IDLE watch start ${accountLogRef(account.id)}")
|
||||
var backoffMs = INITIAL_BACKOFF_MS
|
||||
while (scope.isActive) {
|
||||
try {
|
||||
@@ -212,7 +213,7 @@ class IdleService : Service() {
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "IDLE for ${account.email} dropped; retrying in ${backoffMs}ms", e)
|
||||
AppLog.w(TAG, "IDLE for ${accountLogRef(account.id)} dropped; retrying in ${backoffMs}ms", e)
|
||||
delay(backoffMs)
|
||||
backoffMs = (backoffMs * 2).coerceAtMost(MAX_BACKOFF_MS)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user