fix: wrap IMAP email batch insert in a single transaction

Without a transaction, N individual inserts each re-acquire the SQLite
write lock, creating a window where a concurrent sync-log write hits
SQLITE_LOCKED. The whole batch then throws, no checkpoint is saved, and
the inbox ends up with only the emails that inserted before the failure.
Wrapping in one transaction makes the batch atomic and holds the lock
for a single commit instead of N.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Thomas Güttler
2026-04-21 12:11:44 +02:00
co-authored by Claude Sonnet 4.6
parent 1ab915d73a
commit 2bd082e90e
@@ -393,37 +393,39 @@ class EmailRepositoryImpl implements EmailRepository {
'(UID FLAGS ENVELOPE BODYSTRUCTURE RFC822.SIZE)',
);
var bytes = 0;
for (final msg in fetch.messages) {
final envelope = msg.envelope;
if (envelope == null) {
log('IMAP: skipping message with no envelope (uid=${msg.uid}, mailbox=$mailboxPath)');
continue;
await _db.transaction(() async {
for (final msg in fetch.messages) {
final envelope = msg.envelope;
if (envelope == null) {
log('IMAP: skipping message with no envelope (uid=${msg.uid}, mailbox=$mailboxPath)');
continue;
}
final uid = msg.uid;
if (uid == null) {
log('IMAP: skipping message with no uid (mailbox=$mailboxPath)');
continue;
}
bytes += msg.size ?? 0;
final emailId = '${account.id}:$uid';
await _db.into(_db.emails).insertOnConflictUpdate(
EmailsCompanion.insert(
id: emailId,
accountId: account.id,
mailboxPath: mailboxPath,
uid: uid,
subject: Value(envelope.subject),
sentAt: Value(envelope.date),
receivedAt: envelope.date ?? DateTime.now(),
fromJson: Value(_encodeAddresses(envelope.from)),
toAddresses: Value(_encodeAddresses(envelope.to)),
ccJson: Value(_encodeAddresses(envelope.cc)),
isSeen: Value(msg.flags?.contains(r'\Seen') ?? false),
isFlagged: Value(msg.flags?.contains(r'\Flagged') ?? false),
hasAttachment: Value(msg.hasAttachments()),
),
);
}
final uid = msg.uid;
if (uid == null) {
log('IMAP: skipping message with no uid (mailbox=$mailboxPath)');
continue;
}
bytes += msg.size ?? 0;
final emailId = '${account.id}:$uid';
await _db.into(_db.emails).insertOnConflictUpdate(
EmailsCompanion.insert(
id: emailId,
accountId: account.id,
mailboxPath: mailboxPath,
uid: uid,
subject: Value(envelope.subject),
sentAt: Value(envelope.date),
receivedAt: envelope.date ?? DateTime.now(),
fromJson: Value(_encodeAddresses(envelope.from)),
toAddresses: Value(_encodeAddresses(envelope.to)),
ccJson: Value(_encodeAddresses(envelope.cc)),
isSeen: Value(msg.flags?.contains(r'\Seen') ?? false),
isFlagged: Value(msg.flags?.contains(r'\Flagged') ?? false),
hasAttachment: Value(msg.hasAttachments()),
),
);
}
});
return bytes;
}