fix: format, analyze-fix and update mocks
This commit is contained in:
+218
-216
@@ -388,228 +388,231 @@ class AppDatabase extends _$AppDatabase {
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration => MigrationStrategy(
|
||||
onCreate: (m) async {
|
||||
await m.createAll();
|
||||
await _createEmailFts();
|
||||
},
|
||||
onUpgrade: (m, from, to) async {
|
||||
// NOTE: m.createTable(T) creates the LATEST version of table T.
|
||||
// If you later add a column C to T in version X, you must guard
|
||||
// addColumn(T, T.C) with `if (from >= creationVersionOfT && from < X)`.
|
||||
if (from < 2) {
|
||||
await m.addColumn(accounts, accounts.accountType);
|
||||
await m.addColumn(accounts, accounts.jmapUrl);
|
||||
}
|
||||
if (from < 3) {
|
||||
await m.addColumn(accounts, accounts.username);
|
||||
}
|
||||
if (from < 4) {
|
||||
await m.createTable(drafts);
|
||||
}
|
||||
if (from < 5) {
|
||||
await m.createTable(syncStates);
|
||||
}
|
||||
if (from < 6) {
|
||||
await m.createTable(pendingChanges);
|
||||
}
|
||||
if (from < 7) {
|
||||
await m.createTable(syncLogs);
|
||||
}
|
||||
if (from < 8) {
|
||||
await m.addColumn(mailboxes, mailboxes.role);
|
||||
}
|
||||
if (from < 9) {
|
||||
await m.addColumn(emailBodies, emailBodies.cachedAt);
|
||||
}
|
||||
if (from >= 7 && from < 10) {
|
||||
await m.addColumn(syncLogs, syncLogs.protocol);
|
||||
await m.addColumn(syncLogs, syncLogs.mailboxesSynced);
|
||||
await m.addColumn(syncLogs, syncLogs.pendingFlushed);
|
||||
}
|
||||
if (from >= 7 && from < 11) {
|
||||
await m.addColumn(syncLogs, syncLogs.emailsSkipped);
|
||||
await m.addColumn(syncLogs, syncLogs.bytesTransferred);
|
||||
}
|
||||
if (from < 12) {
|
||||
await m.createTable(syncLogMailboxes);
|
||||
}
|
||||
if (from < 13) {
|
||||
await m.addColumn(accounts, accounts.verbose);
|
||||
if (from >= 7) {
|
||||
await m.addColumn(syncLogs, syncLogs.protocolLog);
|
||||
}
|
||||
}
|
||||
if (from < 14) {
|
||||
await m.addColumn(emails, emails.threadId);
|
||||
await m.addColumn(emails, emails.messageId);
|
||||
await m.addColumn(emails, emails.inReplyTo);
|
||||
await m.addColumn(emails, emails.references);
|
||||
}
|
||||
if (from < 15) {
|
||||
await m.addColumn(accounts, accounts.manageSieveHost);
|
||||
await m.addColumn(accounts, accounts.manageSievePort);
|
||||
await m.addColumn(accounts, accounts.manageSieveSsl);
|
||||
}
|
||||
if (from < 16) {
|
||||
await m.addColumn(accounts, accounts.manageSieveAvailable);
|
||||
}
|
||||
if (from < 17) {
|
||||
await m.createTable(threads);
|
||||
// Populate threads from existing emails.
|
||||
final allRows = await select(emails).get();
|
||||
final groups = <String, List<Email>>{};
|
||||
for (final row in allRows) {
|
||||
final key =
|
||||
'${row.accountId}:${row.mailboxPath}:${row.threadId ?? row.id}';
|
||||
groups.putIfAbsent(key, () => []).add(row);
|
||||
}
|
||||
onCreate: (m) async {
|
||||
await m.createAll();
|
||||
await _createEmailFts();
|
||||
},
|
||||
onUpgrade: (m, from, to) async {
|
||||
// NOTE: m.createTable(T) creates the LATEST version of table T.
|
||||
// If you later add a column C to T in version X, you must guard
|
||||
// addColumn(T, T.C) with `if (from >= creationVersionOfT && from < X)`.
|
||||
if (from < 2) {
|
||||
await m.addColumn(accounts, accounts.accountType);
|
||||
await m.addColumn(accounts, accounts.jmapUrl);
|
||||
}
|
||||
if (from < 3) {
|
||||
await m.addColumn(accounts, accounts.username);
|
||||
}
|
||||
if (from < 4) {
|
||||
await m.createTable(drafts);
|
||||
}
|
||||
if (from < 5) {
|
||||
await m.createTable(syncStates);
|
||||
}
|
||||
if (from < 6) {
|
||||
await m.createTable(pendingChanges);
|
||||
}
|
||||
if (from < 7) {
|
||||
await m.createTable(syncLogs);
|
||||
}
|
||||
if (from < 8) {
|
||||
await m.addColumn(mailboxes, mailboxes.role);
|
||||
}
|
||||
if (from < 9) {
|
||||
await m.addColumn(emailBodies, emailBodies.cachedAt);
|
||||
}
|
||||
if (from >= 7 && from < 10) {
|
||||
await m.addColumn(syncLogs, syncLogs.protocol);
|
||||
await m.addColumn(syncLogs, syncLogs.mailboxesSynced);
|
||||
await m.addColumn(syncLogs, syncLogs.pendingFlushed);
|
||||
}
|
||||
if (from >= 7 && from < 11) {
|
||||
await m.addColumn(syncLogs, syncLogs.emailsSkipped);
|
||||
await m.addColumn(syncLogs, syncLogs.bytesTransferred);
|
||||
}
|
||||
if (from < 12) {
|
||||
await m.createTable(syncLogMailboxes);
|
||||
}
|
||||
if (from < 13) {
|
||||
await m.addColumn(accounts, accounts.verbose);
|
||||
if (from >= 7) {
|
||||
await m.addColumn(syncLogs, syncLogs.protocolLog);
|
||||
}
|
||||
}
|
||||
if (from < 14) {
|
||||
await m.addColumn(emails, emails.threadId);
|
||||
await m.addColumn(emails, emails.messageId);
|
||||
await m.addColumn(emails, emails.inReplyTo);
|
||||
await m.addColumn(emails, emails.references);
|
||||
}
|
||||
if (from < 15) {
|
||||
await m.addColumn(accounts, accounts.manageSieveHost);
|
||||
await m.addColumn(accounts, accounts.manageSievePort);
|
||||
await m.addColumn(accounts, accounts.manageSieveSsl);
|
||||
}
|
||||
if (from < 16) {
|
||||
await m.addColumn(accounts, accounts.manageSieveAvailable);
|
||||
}
|
||||
if (from < 17) {
|
||||
await m.createTable(threads);
|
||||
// Populate threads from existing emails.
|
||||
final allRows = await select(emails).get();
|
||||
final groups = <String, List<Email>>{};
|
||||
for (final row in allRows) {
|
||||
final key =
|
||||
'${row.accountId}:${row.mailboxPath}:${row.threadId ?? row.id}';
|
||||
groups.putIfAbsent(key, () => []).add(row);
|
||||
}
|
||||
|
||||
for (final threadEmails in groups.values) {
|
||||
threadEmails.sort((a, b) {
|
||||
final da = a.sentAt ?? a.receivedAt;
|
||||
final db = b.sentAt ?? b.receivedAt;
|
||||
return da.compareTo(db);
|
||||
});
|
||||
final latest = threadEmails.last;
|
||||
for (final threadEmails in groups.values) {
|
||||
threadEmails.sort((a, b) {
|
||||
final da = a.sentAt ?? a.receivedAt;
|
||||
final db = b.sentAt ?? b.receivedAt;
|
||||
return da.compareTo(db);
|
||||
});
|
||||
final latest = threadEmails.last;
|
||||
|
||||
await into(threads).insert(
|
||||
ThreadsCompanion.insert(
|
||||
id: latest.threadId ?? latest.id,
|
||||
accountId: latest.accountId,
|
||||
mailboxPath: latest.mailboxPath,
|
||||
subject: Value(latest.subject),
|
||||
latestDate: latest.sentAt ?? latest.receivedAt,
|
||||
messageCount: Value(threadEmails.length),
|
||||
hasUnread: Value(threadEmails.any((e) => !e.isSeen)),
|
||||
isFlagged: Value(threadEmails.any((e) => e.isFlagged)),
|
||||
preview: Value(latest.preview),
|
||||
latestEmailId: latest.id,
|
||||
emailIdsJson: Value(
|
||||
jsonEncode(threadEmails.map((e) => e.id).toList()),
|
||||
await into(threads).insert(
|
||||
ThreadsCompanion.insert(
|
||||
id: latest.threadId ?? latest.id,
|
||||
accountId: latest.accountId,
|
||||
mailboxPath: latest.mailboxPath,
|
||||
subject: Value(latest.subject),
|
||||
latestDate: latest.sentAt ?? latest.receivedAt,
|
||||
messageCount: Value(threadEmails.length),
|
||||
hasUnread: Value(threadEmails.any((e) => !e.isSeen)),
|
||||
isFlagged: Value(threadEmails.any((e) => e.isFlagged)),
|
||||
preview: Value(latest.preview),
|
||||
latestEmailId: latest.id,
|
||||
emailIdsJson: Value(
|
||||
jsonEncode(threadEmails.map((e) => e.id).toList()),
|
||||
),
|
||||
participantsJson: Value(
|
||||
latest.fromJson,
|
||||
), // Good enough for migration
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (from < 18) {
|
||||
// Index for sorting email list by date.
|
||||
await m.createIndex(
|
||||
Index(
|
||||
'emails_received_at',
|
||||
'CREATE INDEX emails_received_at ON emails (account_id, mailbox_path, received_at DESC);',
|
||||
),
|
||||
participantsJson: Value(
|
||||
latest.fromJson,
|
||||
), // Good enough for migration
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (from < 18) {
|
||||
// Index for sorting email list by date.
|
||||
await m.createIndex(
|
||||
Index(
|
||||
'emails_received_at',
|
||||
'CREATE INDEX emails_received_at ON emails (account_id, mailbox_path, received_at DESC);',
|
||||
),
|
||||
);
|
||||
// Index for finding emails in a thread.
|
||||
await m.createIndex(
|
||||
Index(
|
||||
'emails_thread_id',
|
||||
'CREATE INDEX emails_thread_id ON emails (account_id, mailbox_path, thread_id);',
|
||||
),
|
||||
);
|
||||
// Index for pending changes queue.
|
||||
await m.createIndex(
|
||||
Index(
|
||||
'pending_changes_account_id',
|
||||
'CREATE INDEX pending_changes_account_id ON pending_changes (account_id);',
|
||||
),
|
||||
);
|
||||
}
|
||||
if (from < 19) {
|
||||
await m.createTable(syncHealth);
|
||||
}
|
||||
if (from < 20) {
|
||||
await m.addColumn(emailBodies, emailBodies.headersJson);
|
||||
}
|
||||
if (from < 21) {
|
||||
await m.createTable(undoActions);
|
||||
}
|
||||
if (from < 22) {
|
||||
final check = await customSelect('PRAGMA table_info(emails)').get();
|
||||
final names = check.map((row) => row.read<String>('name')).toList();
|
||||
);
|
||||
// Index for finding emails in a thread.
|
||||
await m.createIndex(
|
||||
Index(
|
||||
'emails_thread_id',
|
||||
'CREATE INDEX emails_thread_id ON emails (account_id, mailbox_path, thread_id);',
|
||||
),
|
||||
);
|
||||
// Index for pending changes queue.
|
||||
await m.createIndex(
|
||||
Index(
|
||||
'pending_changes_account_id',
|
||||
'CREATE INDEX pending_changes_account_id ON pending_changes (account_id);',
|
||||
),
|
||||
);
|
||||
}
|
||||
if (from < 19) {
|
||||
await m.createTable(syncHealth);
|
||||
}
|
||||
if (from < 20) {
|
||||
await m.addColumn(emailBodies, emailBodies.headersJson);
|
||||
}
|
||||
if (from < 21) {
|
||||
await m.createTable(undoActions);
|
||||
}
|
||||
if (from < 22) {
|
||||
final check = await customSelect('PRAGMA table_info(emails)').get();
|
||||
final names = check.map((row) => row.read<String>('name')).toList();
|
||||
|
||||
if (!names.contains('snoozed_until')) {
|
||||
await m.addColumn(emails, emails.snoozedUntil);
|
||||
}
|
||||
if (!names.contains('snoozed_from_mailbox_path')) {
|
||||
await m.addColumn(emails, emails.snoozedFromMailboxPath);
|
||||
}
|
||||
if (!names.contains('snoozed_until')) {
|
||||
await m.addColumn(emails, emails.snoozedUntil);
|
||||
}
|
||||
if (!names.contains('snoozed_from_mailbox_path')) {
|
||||
await m.addColumn(emails, emails.snoozedFromMailboxPath);
|
||||
}
|
||||
|
||||
await m.createIndex(
|
||||
Index(
|
||||
'emails_snoozed_until',
|
||||
'CREATE INDEX IF NOT EXISTS emails_snoozed_until ON emails (account_id, snoozed_until) WHERE snoozed_until IS NOT NULL;',
|
||||
),
|
||||
);
|
||||
}
|
||||
if (from < 23) {
|
||||
await m.addColumn(emails, emails.listUnsubscribeHeader);
|
||||
}
|
||||
if (from >= 4 && from < 24) {
|
||||
await m.addColumn(drafts, drafts.imapServerId);
|
||||
}
|
||||
if (from < 25) {
|
||||
// For observeMailboxes: filter by account_id, sort by path.
|
||||
await m.createIndex(
|
||||
Index(
|
||||
'mailboxes_account_id',
|
||||
'CREATE INDEX IF NOT EXISTS mailboxes_account_id ON mailboxes (account_id, path);',
|
||||
),
|
||||
);
|
||||
// For observeThreads: filter by account_id+mailbox_path, sort by latest_date.
|
||||
await m.createIndex(
|
||||
Index(
|
||||
'threads_latest_date',
|
||||
'CREATE INDEX IF NOT EXISTS threads_latest_date ON threads (account_id, mailbox_path, latest_date DESC);',
|
||||
),
|
||||
);
|
||||
}
|
||||
if (from < 26) {
|
||||
await _createEmailFts();
|
||||
// Backfill FTS index from existing rows.
|
||||
await customStatement('''
|
||||
await m.createIndex(
|
||||
Index(
|
||||
'emails_snoozed_until',
|
||||
'CREATE INDEX IF NOT EXISTS emails_snoozed_until ON emails (account_id, snoozed_until) WHERE snoozed_until IS NOT NULL;',
|
||||
),
|
||||
);
|
||||
}
|
||||
if (from < 23) {
|
||||
await m.addColumn(emails, emails.listUnsubscribeHeader);
|
||||
}
|
||||
if (from >= 4 && from < 24) {
|
||||
await m.addColumn(drafts, drafts.imapServerId);
|
||||
}
|
||||
if (from < 25) {
|
||||
// For observeMailboxes: filter by account_id, sort by path.
|
||||
await m.createIndex(
|
||||
Index(
|
||||
'mailboxes_account_id',
|
||||
'CREATE INDEX IF NOT EXISTS mailboxes_account_id ON mailboxes (account_id, path);',
|
||||
),
|
||||
);
|
||||
// For observeThreads: filter by account_id+mailbox_path, sort by latest_date.
|
||||
await m.createIndex(
|
||||
Index(
|
||||
'threads_latest_date',
|
||||
'CREATE INDEX IF NOT EXISTS threads_latest_date ON threads (account_id, mailbox_path, latest_date DESC);',
|
||||
),
|
||||
);
|
||||
}
|
||||
if (from < 26) {
|
||||
await _createEmailFts();
|
||||
// Backfill FTS index from existing rows.
|
||||
await customStatement('''
|
||||
INSERT INTO email_fts(rowid, subject, preview, from_json)
|
||||
SELECT rowid, subject, preview, from_json FROM emails
|
||||
''');
|
||||
}
|
||||
if (from < 27) {
|
||||
await m.createTable(searchHistoryEntries);
|
||||
}
|
||||
if (from < 28) {
|
||||
await m.addColumn(emailBodies, emailBodies.mimeTreeJson);
|
||||
}
|
||||
if (from < 29) {
|
||||
await m.createTable(localSieveScripts);
|
||||
}
|
||||
if (from >= 12 && from < 30) {
|
||||
await m.addColumn(syncLogMailboxes, syncLogMailboxes.durationMs);
|
||||
}
|
||||
if (from < 31) {
|
||||
await m.createTable(shareKeys);
|
||||
}
|
||||
if (from < 32) {
|
||||
await m.createTable(localSieveApplied);
|
||||
}
|
||||
if (from >= 7 && from < 33) {
|
||||
await m.addColumn(syncLogs, syncLogs.errorStackTrace);
|
||||
await m.addColumn(syncLogs, syncLogs.isPermanent);
|
||||
}
|
||||
if (from < 34) {
|
||||
await m.createTable(userPreferences);
|
||||
}
|
||||
if (from >= 34 && from < 35) {
|
||||
await m.addColumn(
|
||||
userPreferences,
|
||||
userPreferences.mailViewButtonPosition,
|
||||
);
|
||||
}
|
||||
if (from >= 34 && from < 36) {
|
||||
await m.addColumn(userPreferences, userPreferences.afterMailViewAction);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
if (from < 27) {
|
||||
await m.createTable(searchHistoryEntries);
|
||||
}
|
||||
if (from < 28) {
|
||||
await m.addColumn(emailBodies, emailBodies.mimeTreeJson);
|
||||
}
|
||||
if (from < 29) {
|
||||
await m.createTable(localSieveScripts);
|
||||
}
|
||||
if (from >= 12 && from < 30) {
|
||||
await m.addColumn(syncLogMailboxes, syncLogMailboxes.durationMs);
|
||||
}
|
||||
if (from < 31) {
|
||||
await m.createTable(shareKeys);
|
||||
}
|
||||
if (from < 32) {
|
||||
await m.createTable(localSieveApplied);
|
||||
}
|
||||
if (from >= 7 && from < 33) {
|
||||
await m.addColumn(syncLogs, syncLogs.errorStackTrace);
|
||||
await m.addColumn(syncLogs, syncLogs.isPermanent);
|
||||
}
|
||||
if (from < 34) {
|
||||
await m.createTable(userPreferences);
|
||||
}
|
||||
if (from >= 34 && from < 35) {
|
||||
await m.addColumn(
|
||||
userPreferences,
|
||||
userPreferences.mailViewButtonPosition,
|
||||
);
|
||||
}
|
||||
if (from >= 34 && from < 36) {
|
||||
await m.addColumn(
|
||||
userPreferences,
|
||||
userPreferences.afterMailViewAction,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Resolved once in main() via initDatabasePath() before runApp().
|
||||
@@ -660,8 +663,7 @@ Future<String> _resolveDatabasePath() async {
|
||||
}
|
||||
throw PlatformException(
|
||||
code: 'channel-error',
|
||||
message:
|
||||
'path_provider unavailable after ${delays.length + 1} attempts — '
|
||||
message: 'path_provider unavailable after ${delays.length + 1} attempts — '
|
||||
'cannot open database.',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ class LocalSieveRepository {
|
||||
Future<List<SieveScript>> listScripts(String accountId) async {
|
||||
final rows = await (_db.select(
|
||||
_db.localSieveScripts,
|
||||
)..where((t) => t.accountId.equals(accountId))).get();
|
||||
)..where((t) => t.accountId.equals(accountId)))
|
||||
.get();
|
||||
return rows
|
||||
.map(
|
||||
(r) => SieveScript(
|
||||
@@ -26,11 +27,10 @@ class LocalSieveRepository {
|
||||
|
||||
Future<String> getScriptContent(String accountId, String blobId) async {
|
||||
final rowId = int.parse(blobId);
|
||||
final row =
|
||||
await (_db.select(
|
||||
_db.localSieveScripts,
|
||||
)..where((t) => t.id.equals(rowId) & t.accountId.equals(accountId)))
|
||||
.getSingleOrNull();
|
||||
final row = await (_db.select(
|
||||
_db.localSieveScripts,
|
||||
)..where((t) => t.id.equals(rowId) & t.accountId.equals(accountId)))
|
||||
.getSingleOrNull();
|
||||
if (row == null) throw Exception('Local script not found: $blobId');
|
||||
return row.content;
|
||||
}
|
||||
@@ -46,16 +46,16 @@ class LocalSieveRepository {
|
||||
await (_db.update(_db.localSieveScripts)
|
||||
..where((t) => t.id.equals(rowId) & t.accountId.equals(accountId)))
|
||||
.write(
|
||||
LocalSieveScriptsCompanion(
|
||||
name: Value(name),
|
||||
content: Value(content),
|
||||
),
|
||||
);
|
||||
final updated =
|
||||
await (_db.select(_db.localSieveScripts)..where(
|
||||
(t) => t.id.equals(rowId) & t.accountId.equals(accountId),
|
||||
))
|
||||
.getSingleOrNull();
|
||||
LocalSieveScriptsCompanion(
|
||||
name: Value(name),
|
||||
content: Value(content),
|
||||
),
|
||||
);
|
||||
final updated = await (_db.select(_db.localSieveScripts)
|
||||
..where(
|
||||
(t) => t.id.equals(rowId) & t.accountId.equals(accountId),
|
||||
))
|
||||
.getSingleOrNull();
|
||||
return SieveScript(
|
||||
id: id,
|
||||
name: name,
|
||||
@@ -63,9 +63,7 @@ class LocalSieveRepository {
|
||||
isActive: updated?.isActive ?? false,
|
||||
);
|
||||
}
|
||||
final rowId = await _db
|
||||
.into(_db.localSieveScripts)
|
||||
.insert(
|
||||
final rowId = await _db.into(_db.localSieveScripts).insert(
|
||||
LocalSieveScriptsCompanion.insert(
|
||||
accountId: accountId,
|
||||
name: name,
|
||||
@@ -80,7 +78,8 @@ class LocalSieveRepository {
|
||||
final rowId = int.parse(scriptId);
|
||||
await (_db.delete(
|
||||
_db.localSieveScripts,
|
||||
)..where((t) => t.id.equals(rowId) & t.accountId.equals(accountId))).go();
|
||||
)..where((t) => t.id.equals(rowId) & t.accountId.equals(accountId)))
|
||||
.go();
|
||||
}
|
||||
|
||||
Future<void> activateScript(String accountId, String scriptId) async {
|
||||
|
||||
@@ -6,12 +6,11 @@ import 'package:sharedinbox/core/models/account.dart';
|
||||
import 'package:sharedinbox/core/utils/host_utils.dart';
|
||||
import 'package:sharedinbox/data/imap/tls_error.dart';
|
||||
|
||||
typedef ImapConnectFn =
|
||||
Future<ImapClient> Function(
|
||||
Account account,
|
||||
String username,
|
||||
String password,
|
||||
);
|
||||
typedef ImapConnectFn = Future<ImapClient> Function(
|
||||
Account account,
|
||||
String username,
|
||||
String password,
|
||||
);
|
||||
|
||||
/// Zone value key signalling that a [StringBuffer] for protocol logging is
|
||||
/// active. When this key is non-null in the current zone, [connectImap]
|
||||
@@ -65,9 +64,8 @@ Future<SmtpClient> connectSmtp(
|
||||
// clientDomain is the sending domain advertised in EHLO — use the host part
|
||||
// of the sender email, falling back to the SMTP host.
|
||||
final atIndex = account.email.lastIndexOf('@');
|
||||
final clientDomain = atIndex != -1
|
||||
? account.email.substring(atIndex + 1)
|
||||
: account.smtpHost;
|
||||
final clientDomain =
|
||||
atIndex != -1 ? account.email.substring(atIndex + 1) : account.smtpHost;
|
||||
|
||||
if (!account.smtpSsl && !isLocalhost(account.smtpHost)) {
|
||||
throw Exception(
|
||||
|
||||
@@ -26,14 +26,14 @@ class JmapClient {
|
||||
String? uploadUrl,
|
||||
String? downloadUrl,
|
||||
String? eventSourceUrl,
|
||||
}) : _httpClient = httpClient,
|
||||
_credentials = credentials,
|
||||
_apiUrl = apiUrl,
|
||||
_accountId = accountId,
|
||||
_capabilities = capabilities,
|
||||
_uploadUrl = uploadUrl,
|
||||
_downloadUrl = downloadUrl,
|
||||
_eventSourceUrl = eventSourceUrl;
|
||||
}) : _httpClient = httpClient,
|
||||
_credentials = credentials,
|
||||
_apiUrl = apiUrl,
|
||||
_accountId = accountId,
|
||||
_capabilities = capabilities,
|
||||
_uploadUrl = uploadUrl,
|
||||
_downloadUrl = downloadUrl,
|
||||
_eventSourceUrl = eventSourceUrl;
|
||||
|
||||
final http.Client _httpClient;
|
||||
final String _credentials;
|
||||
@@ -67,9 +67,12 @@ class JmapClient {
|
||||
http.Response resp;
|
||||
var attempt = 0;
|
||||
while (true) {
|
||||
resp = await httpClient
|
||||
.get(jmapUrl, headers: {'Authorization': 'Basic $credentials'})
|
||||
.timeout(const Duration(seconds: 10));
|
||||
resp = await httpClient.get(
|
||||
jmapUrl,
|
||||
headers: {
|
||||
'Authorization': 'Basic $credentials',
|
||||
},
|
||||
).timeout(const Duration(seconds: 10));
|
||||
if (resp.statusCode != 429 || attempt >= 4) {
|
||||
break;
|
||||
}
|
||||
@@ -215,9 +218,12 @@ class JmapClient {
|
||||
.replaceAll('{name}', Uri.encodeComponent(name))
|
||||
.replaceAll('{type}', Uri.encodeComponent(type)),
|
||||
);
|
||||
final resp = await _httpClient
|
||||
.get(url, headers: {'Authorization': 'Basic $_credentials'})
|
||||
.timeout(const Duration(seconds: 30));
|
||||
final resp = await _httpClient.get(
|
||||
url,
|
||||
headers: {
|
||||
'Authorization': 'Basic $_credentials',
|
||||
},
|
||||
).timeout(const Duration(seconds: 30));
|
||||
if (resp.statusCode != 200) {
|
||||
throw JmapException('Blob download failed (HTTP ${resp.statusCode})');
|
||||
}
|
||||
@@ -240,8 +246,7 @@ class JmapClient {
|
||||
|
||||
static String _extractAccountId(Map<String, dynamic> session) {
|
||||
final primaryAccounts = session['primaryAccounts'] as Map<String, dynamic>?;
|
||||
final id =
|
||||
primaryAccounts?['urn:ietf:params:jmap:mail'] as String? ??
|
||||
final id = primaryAccounts?['urn:ietf:params:jmap:mail'] as String? ??
|
||||
primaryAccounts?['urn:ietf:params:jmap:core'] as String?;
|
||||
if (id != null) return id;
|
||||
|
||||
|
||||
@@ -9,18 +9,18 @@ import 'package:sharedinbox/core/repositories/account_repository.dart';
|
||||
import 'package:sharedinbox/data/imap/managesieve_client.dart';
|
||||
import 'package:sharedinbox/data/jmap/jmap_client.dart';
|
||||
|
||||
typedef ManageSieveConnectFn =
|
||||
Future<ManageSieveClient> Function({
|
||||
required String host,
|
||||
required int port,
|
||||
required bool useTls,
|
||||
});
|
||||
typedef ManageSieveConnectFn = Future<ManageSieveClient> Function({
|
||||
required String host,
|
||||
required int port,
|
||||
required bool useTls,
|
||||
});
|
||||
|
||||
Future<ManageSieveClient> _defaultManageSieveConnect({
|
||||
required String host,
|
||||
required int port,
|
||||
required bool useTls,
|
||||
}) => ManageSieveClient.connect(host: host, port: port, useTls: useTls);
|
||||
}) =>
|
||||
ManageSieveClient.connect(host: host, port: port, useTls: useTls);
|
||||
|
||||
class SieveRepository {
|
||||
SieveRepository(
|
||||
@@ -51,13 +51,16 @@ class SieveRepository {
|
||||
});
|
||||
}
|
||||
return _withJmap(account, (jmap) async {
|
||||
final responses = await jmap.call([
|
||||
final responses = await jmap.call(
|
||||
[
|
||||
'SieveScript/get',
|
||||
{'accountId': jmap.accountId, 'ids': null},
|
||||
'0',
|
||||
[
|
||||
'SieveScript/get',
|
||||
{'accountId': jmap.accountId, 'ids': null},
|
||||
'0',
|
||||
],
|
||||
],
|
||||
], withSieve: true);
|
||||
withSieve: true,
|
||||
);
|
||||
final result = _responseArgs(responses, 0, 'SieveScript/get');
|
||||
final list = result['list'] as List<dynamic>;
|
||||
return list.map((e) {
|
||||
@@ -123,9 +126,12 @@ class SieveRepository {
|
||||
id: {'name': name, 'blobId': blobId},
|
||||
},
|
||||
};
|
||||
final responses = await jmap.call([
|
||||
['SieveScript/set', setArgs, '0'],
|
||||
], withSieve: true);
|
||||
final responses = await jmap.call(
|
||||
[
|
||||
['SieveScript/set', setArgs, '0'],
|
||||
],
|
||||
withSieve: true,
|
||||
);
|
||||
final result = _responseArgs(responses, 0, 'SieveScript/set');
|
||||
if (id == null) {
|
||||
final created = result['created'] as Map<String, dynamic>?;
|
||||
@@ -164,16 +170,19 @@ class SieveRepository {
|
||||
return;
|
||||
}
|
||||
await _withJmap(account, (jmap) async {
|
||||
final responses = await jmap.call([
|
||||
final responses = await jmap.call(
|
||||
[
|
||||
'SieveScript/set',
|
||||
{
|
||||
'accountId': jmap.accountId,
|
||||
'destroy': [scriptId],
|
||||
},
|
||||
'0',
|
||||
[
|
||||
'SieveScript/set',
|
||||
{
|
||||
'accountId': jmap.accountId,
|
||||
'destroy': [scriptId],
|
||||
},
|
||||
'0',
|
||||
],
|
||||
],
|
||||
], withSieve: true);
|
||||
withSieve: true,
|
||||
);
|
||||
final result = _responseArgs(responses, 0, 'SieveScript/set');
|
||||
final notDestroyed = result['notDestroyed'] as Map<String, dynamic>?;
|
||||
if (notDestroyed != null && notDestroyed.containsKey(scriptId)) {
|
||||
@@ -192,13 +201,16 @@ class SieveRepository {
|
||||
return;
|
||||
}
|
||||
await _withJmap(account, (jmap) async {
|
||||
await jmap.call([
|
||||
await jmap.call(
|
||||
[
|
||||
'SieveScript/activate',
|
||||
{'accountId': jmap.accountId, 'id': scriptId},
|
||||
'0',
|
||||
[
|
||||
'SieveScript/activate',
|
||||
{'accountId': jmap.accountId, 'id': scriptId},
|
||||
'0',
|
||||
],
|
||||
],
|
||||
], withSieve: true);
|
||||
withSieve: true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -219,9 +231,8 @@ class SieveRepository {
|
||||
throw Exception('Account has no JMAP URL');
|
||||
}
|
||||
final password = await _accounts.getPassword(account.id);
|
||||
final username = account.username.isNotEmpty
|
||||
? account.username
|
||||
: account.email;
|
||||
final username =
|
||||
account.username.isNotEmpty ? account.username : account.email;
|
||||
final jmap = await JmapClient.connect(
|
||||
httpClient: _httpClient,
|
||||
jmapUrl: Uri.parse(jmapUrl),
|
||||
@@ -247,9 +258,8 @@ class SieveRepository {
|
||||
throw Exception('Account has no ManageSieve host configured');
|
||||
}
|
||||
final password = await _accounts.getPassword(account.id);
|
||||
final username = account.username.isNotEmpty
|
||||
? account.username
|
||||
: account.email;
|
||||
final username =
|
||||
account.username.isNotEmpty ? account.username : account.email;
|
||||
final client = await _manageSieveConnect(
|
||||
host: host,
|
||||
port: account.manageSievePort,
|
||||
|
||||
@@ -23,15 +23,14 @@ class AccountRepositoryImpl implements AccountRepository {
|
||||
Future<model.Account?> getAccount(String id) async {
|
||||
final row = await (_db.select(
|
||||
_db.accounts,
|
||||
)..where((t) => t.id.equals(id))).getSingleOrNull();
|
||||
)..where((t) => t.id.equals(id)))
|
||||
.getSingleOrNull();
|
||||
return row == null ? null : _toModel(row);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> addAccount(model.Account account, String password) async {
|
||||
await _db
|
||||
.into(_db.accounts)
|
||||
.insertOnConflictUpdate(
|
||||
await _db.into(_db.accounts).insertOnConflictUpdate(
|
||||
AccountsCompanion.insert(
|
||||
id: account.id,
|
||||
displayName: account.displayName,
|
||||
@@ -59,7 +58,8 @@ class AccountRepositoryImpl implements AccountRepository {
|
||||
Future<void> updateAccount(model.Account account, {String? password}) async {
|
||||
await (_db.update(
|
||||
_db.accounts,
|
||||
)..where((t) => t.id.equals(account.id))).write(
|
||||
)..where((t) => t.id.equals(account.id)))
|
||||
.write(
|
||||
AccountsCompanion(
|
||||
displayName: Value(account.displayName),
|
||||
email: Value(account.email),
|
||||
@@ -102,22 +102,22 @@ class AccountRepositoryImpl implements AccountRepository {
|
||||
String _passwordKey(String accountId) => 'account_password_$accountId';
|
||||
|
||||
model.Account _toModel(Account row) => model.Account(
|
||||
id: row.id,
|
||||
displayName: row.displayName,
|
||||
email: row.email,
|
||||
username: row.username,
|
||||
type: model.AccountType.values.byName(row.accountType),
|
||||
imapHost: row.imapHost,
|
||||
imapPort: row.imapPort,
|
||||
imapSsl: row.imapSsl,
|
||||
smtpHost: row.smtpHost,
|
||||
smtpPort: row.smtpPort,
|
||||
smtpSsl: row.smtpSsl,
|
||||
manageSieveHost: row.manageSieveHost,
|
||||
manageSievePort: row.manageSievePort,
|
||||
manageSieveSsl: row.manageSieveSsl,
|
||||
manageSieveAvailable: row.manageSieveAvailable,
|
||||
jmapUrl: row.jmapUrl,
|
||||
verbose: row.verbose,
|
||||
);
|
||||
id: row.id,
|
||||
displayName: row.displayName,
|
||||
email: row.email,
|
||||
username: row.username,
|
||||
type: model.AccountType.values.byName(row.accountType),
|
||||
imapHost: row.imapHost,
|
||||
imapPort: row.imapPort,
|
||||
imapSsl: row.imapSsl,
|
||||
smtpHost: row.smtpHost,
|
||||
smtpPort: row.smtpPort,
|
||||
smtpSsl: row.smtpSsl,
|
||||
manageSieveHost: row.manageSieveHost,
|
||||
manageSievePort: row.manageSievePort,
|
||||
manageSieveSsl: row.manageSieveSsl,
|
||||
manageSieveAvailable: row.manageSieveAvailable,
|
||||
jmapUrl: row.jmapUrl,
|
||||
verbose: row.verbose,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import 'package:sharedinbox/data/imap/imap_client_factory.dart';
|
||||
|
||||
class DraftRepositoryImpl implements DraftRepository {
|
||||
DraftRepositoryImpl(this._db, this._accounts, {ImapConnectFn? imapConnect})
|
||||
: _imapConnect = imapConnect;
|
||||
: _imapConnect = imapConnect;
|
||||
|
||||
final AppDatabase _db;
|
||||
final AccountRepository _accounts;
|
||||
@@ -51,9 +51,7 @@ class DraftRepositoryImpl implements DraftRepository {
|
||||
);
|
||||
}
|
||||
|
||||
final newId = await _db
|
||||
.into(_db.drafts)
|
||||
.insert(
|
||||
final newId = await _db.into(_db.drafts).insert(
|
||||
DraftsCompanion.insert(
|
||||
accountId: Value(accountId),
|
||||
replyToEmailId: Value(replyToEmailId),
|
||||
@@ -94,7 +92,8 @@ class DraftRepositoryImpl implements DraftRepository {
|
||||
Future<SavedDraft?> getDraft(int id) async {
|
||||
final row = await (_db.select(
|
||||
_db.drafts,
|
||||
)..where((t) => t.id.equals(id))).getSingleOrNull();
|
||||
)..where((t) => t.id.equals(id)))
|
||||
.getSingleOrNull();
|
||||
return row == null ? null : _toModel(row);
|
||||
}
|
||||
|
||||
@@ -111,9 +110,8 @@ class DraftRepositoryImpl implements DraftRepository {
|
||||
final account = await _accounts.getAccount(accountId);
|
||||
if (account == null || account.type != AccountType.imap) return;
|
||||
|
||||
final username = account.username.isNotEmpty
|
||||
? account.username
|
||||
: account.email;
|
||||
final username =
|
||||
account.username.isNotEmpty ? account.username : account.email;
|
||||
imap.ImapClient? client;
|
||||
try {
|
||||
client = await connect(account, username, password);
|
||||
@@ -134,11 +132,11 @@ class DraftRepositoryImpl implements DraftRepository {
|
||||
final messageCount = selectResult.messagesExists;
|
||||
|
||||
// Upload local drafts that have no server counterpart.
|
||||
final localDrafts =
|
||||
await (_db.select(_db.drafts)..where(
|
||||
(t) => t.accountId.equals(accountId) & t.imapServerId.isNull(),
|
||||
))
|
||||
.get();
|
||||
final localDrafts = await (_db.select(_db.drafts)
|
||||
..where(
|
||||
(t) => t.accountId.equals(accountId) & t.imapServerId.isNull(),
|
||||
))
|
||||
.get();
|
||||
|
||||
for (final row in localDrafts) {
|
||||
final builder = imap.MessageBuilder()
|
||||
@@ -152,8 +150,8 @@ class DraftRepositoryImpl implements DraftRepository {
|
||||
targetMailboxPath: 'Drafts',
|
||||
flags: [r'\Draft'],
|
||||
);
|
||||
final uidList = appendResult.responseCodeAppendUid?.targetSequence
|
||||
.toList();
|
||||
final uidList =
|
||||
appendResult.responseCodeAppendUid?.targetSequence.toList();
|
||||
final uid = (uidList != null && uidList.isNotEmpty)
|
||||
? uidList.first.toString()
|
||||
: null;
|
||||
@@ -166,12 +164,11 @@ class DraftRepositoryImpl implements DraftRepository {
|
||||
|
||||
// Download server drafts not tracked locally.
|
||||
if (messageCount > 0) {
|
||||
final knownServerIds =
|
||||
await (_db.select(_db.drafts)..where(
|
||||
(t) =>
|
||||
t.accountId.equals(accountId) & t.imapServerId.isNotNull(),
|
||||
))
|
||||
.get();
|
||||
final knownServerIds = await (_db.select(_db.drafts)
|
||||
..where(
|
||||
(t) => t.accountId.equals(accountId) & t.imapServerId.isNotNull(),
|
||||
))
|
||||
.get();
|
||||
final knownIds = knownServerIds.map((r) => r.imapServerId!).toSet();
|
||||
|
||||
final seq = imap.MessageSequence.fromAll();
|
||||
@@ -182,9 +179,7 @@ class DraftRepositoryImpl implements DraftRepository {
|
||||
if (msg.flags?.contains(r'\Deleted') ?? false) continue;
|
||||
final env = msg.envelope;
|
||||
final now = DateTime.now();
|
||||
await _db
|
||||
.into(_db.drafts)
|
||||
.insert(
|
||||
await _db.into(_db.drafts).insert(
|
||||
DraftsCompanion.insert(
|
||||
accountId: Value(accountId),
|
||||
toText: Value(_addressListToText(env?.to)),
|
||||
@@ -210,14 +205,14 @@ class DraftRepositoryImpl implements DraftRepository {
|
||||
}
|
||||
|
||||
SavedDraft _toModel(Draft row) => SavedDraft(
|
||||
id: row.id,
|
||||
accountId: row.accountId,
|
||||
replyToEmailId: row.replyToEmailId,
|
||||
toText: row.toText,
|
||||
ccText: row.ccText,
|
||||
subjectText: row.subjectText,
|
||||
bodyText: row.bodyText,
|
||||
updatedAt: row.updatedAt,
|
||||
imapServerId: row.imapServerId,
|
||||
);
|
||||
id: row.id,
|
||||
accountId: row.accountId,
|
||||
replyToEmailId: row.replyToEmailId,
|
||||
toText: row.toText,
|
||||
ccText: row.ccText,
|
||||
subjectText: row.subjectText,
|
||||
bodyText: row.bodyText,
|
||||
updatedAt: row.updatedAt,
|
||||
imapServerId: row.imapServerId,
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,8 +17,8 @@ class MailboxRepositoryImpl implements MailboxRepository {
|
||||
this._accounts, {
|
||||
ImapConnectFn imapConnect = connectImap,
|
||||
http.Client? httpClient,
|
||||
}) : _imapConnect = imapConnect,
|
||||
_httpClient = httpClient ?? http.Client();
|
||||
}) : _imapConnect = imapConnect,
|
||||
_httpClient = httpClient ?? http.Client();
|
||||
|
||||
final AppDatabase _db;
|
||||
final AccountRepository _accounts;
|
||||
@@ -45,13 +45,12 @@ class MailboxRepositoryImpl implements MailboxRepository {
|
||||
String accountId,
|
||||
String role,
|
||||
) async {
|
||||
final row =
|
||||
await (_db.select(_db.mailboxes)
|
||||
..where(
|
||||
(t) => t.accountId.equals(accountId) & t.role.equals(role),
|
||||
)
|
||||
..limit(1))
|
||||
.getSingleOrNull();
|
||||
final row = await (_db.select(_db.mailboxes)
|
||||
..where(
|
||||
(t) => t.accountId.equals(accountId) & t.role.equals(role),
|
||||
)
|
||||
..limit(1))
|
||||
.getSingleOrNull();
|
||||
return row == null ? null : _toModel(row);
|
||||
}
|
||||
|
||||
@@ -85,7 +84,8 @@ class MailboxRepositoryImpl implements MailboxRepository {
|
||||
// folders the server doesn't tag with a special-use attribute.
|
||||
final existingRows = await (_db.select(
|
||||
_db.mailboxes,
|
||||
)..where((t) => t.accountId.equals(account.id))).get();
|
||||
)..where((t) => t.accountId.equals(account.id)))
|
||||
.get();
|
||||
final existingRoles = {for (final r in existingRows) r.id: r.role};
|
||||
|
||||
for (final mb in mailboxes) {
|
||||
@@ -111,9 +111,7 @@ class MailboxRepositoryImpl implements MailboxRepository {
|
||||
// when the IMAP server does not expose a special-use attribute.
|
||||
final role = _imapRole(mb) ?? existingRoles[id];
|
||||
|
||||
await _db
|
||||
.into(_db.mailboxes)
|
||||
.insertOnConflictUpdate(
|
||||
await _db.into(_db.mailboxes).insertOnConflictUpdate(
|
||||
MailboxesCompanion.insert(
|
||||
id: id,
|
||||
accountId: account.id,
|
||||
@@ -218,7 +216,8 @@ class MailboxRepositoryImpl implements MailboxRepository {
|
||||
for (final jmapId in destroyed) {
|
||||
await (_db.delete(
|
||||
_db.mailboxes,
|
||||
)..where((t) => t.id.equals('$accountId:$jmapId'))).go();
|
||||
)..where((t) => t.id.equals('$accountId:$jmapId')))
|
||||
.go();
|
||||
}
|
||||
|
||||
await _saveSyncState(accountId, 'Mailbox', newState);
|
||||
@@ -239,9 +238,7 @@ class MailboxRepositoryImpl implements MailboxRepository {
|
||||
final dbId = '$accountId:$jmapId';
|
||||
// For JMAP accounts, path stores the JMAP mailbox ID so that
|
||||
// Email rows can reference it via mailboxPath.
|
||||
await _db
|
||||
.into(_db.mailboxes)
|
||||
.insertOnConflictUpdate(
|
||||
await _db.into(_db.mailboxes).insertOnConflictUpdate(
|
||||
MailboxesCompanion.insert(
|
||||
id: dbId,
|
||||
accountId: accountId,
|
||||
@@ -258,13 +255,13 @@ class MailboxRepositoryImpl implements MailboxRepository {
|
||||
// ── sync_state helpers ────────────────────────────────────────────────────
|
||||
|
||||
Future<String?> _loadSyncState(String accountId, String resourceType) async {
|
||||
final row =
|
||||
await (_db.select(_db.syncStates)..where(
|
||||
(t) =>
|
||||
t.accountId.equals(accountId) &
|
||||
t.resourceType.equals(resourceType),
|
||||
))
|
||||
.getSingleOrNull();
|
||||
final row = await (_db.select(_db.syncStates)
|
||||
..where(
|
||||
(t) =>
|
||||
t.accountId.equals(accountId) &
|
||||
t.resourceType.equals(resourceType),
|
||||
))
|
||||
.getSingleOrNull();
|
||||
return row?.state;
|
||||
}
|
||||
|
||||
@@ -273,9 +270,7 @@ class MailboxRepositoryImpl implements MailboxRepository {
|
||||
String resourceType,
|
||||
String state,
|
||||
) async {
|
||||
await _db
|
||||
.into(_db.syncStates)
|
||||
.insertOnConflictUpdate(
|
||||
await _db.into(_db.syncStates).insertOnConflictUpdate(
|
||||
SyncStatesCompanion.insert(
|
||||
accountId: accountId,
|
||||
resourceType: resourceType,
|
||||
@@ -304,14 +299,14 @@ class MailboxRepositoryImpl implements MailboxRepository {
|
||||
}
|
||||
|
||||
model.Mailbox _toModel(MailboxRow row) => model.Mailbox(
|
||||
id: row.id,
|
||||
accountId: row.accountId,
|
||||
path: row.path,
|
||||
name: row.name,
|
||||
unreadCount: row.unreadCount,
|
||||
totalCount: row.totalCount,
|
||||
role: row.role,
|
||||
);
|
||||
id: row.id,
|
||||
accountId: row.accountId,
|
||||
path: row.path,
|
||||
name: row.name,
|
||||
unreadCount: row.unreadCount,
|
||||
totalCount: row.totalCount,
|
||||
role: row.role,
|
||||
);
|
||||
|
||||
/// Maps enough_mail special-use flags (RFC 6154) to JMAP role strings (RFC 8621).
|
||||
static String? _imapRole(imap.Mailbox mb) {
|
||||
@@ -328,7 +323,8 @@ class MailboxRepositoryImpl implements MailboxRepository {
|
||||
Future<void> clearForResync(String accountId) async {
|
||||
await (_db.delete(
|
||||
_db.mailboxes,
|
||||
)..where((t) => t.accountId.equals(accountId))).go();
|
||||
)..where((t) => t.accountId.equals(accountId)))
|
||||
.go();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -364,9 +360,7 @@ class MailboxRepositoryImpl implements MailboxRepository {
|
||||
await client.logout();
|
||||
}
|
||||
final id = '${account.id}:$name';
|
||||
await _db
|
||||
.into(_db.mailboxes)
|
||||
.insertOnConflictUpdate(
|
||||
await _db.into(_db.mailboxes).insertOnConflictUpdate(
|
||||
MailboxesCompanion.insert(
|
||||
id: id,
|
||||
accountId: account.id,
|
||||
@@ -377,7 +371,8 @@ class MailboxRepositoryImpl implements MailboxRepository {
|
||||
);
|
||||
final row = await (_db.select(
|
||||
_db.mailboxes,
|
||||
)..where((t) => t.id.equals(id))).getSingle();
|
||||
)..where((t) => t.id.equals(id)))
|
||||
.getSingle();
|
||||
return _toModel(row);
|
||||
}
|
||||
|
||||
@@ -419,9 +414,7 @@ class MailboxRepositoryImpl implements MailboxRepository {
|
||||
);
|
||||
}
|
||||
final dbId = '${account.id}:$newId';
|
||||
await _db
|
||||
.into(_db.mailboxes)
|
||||
.insertOnConflictUpdate(
|
||||
await _db.into(_db.mailboxes).insertOnConflictUpdate(
|
||||
MailboxesCompanion.insert(
|
||||
id: dbId,
|
||||
accountId: account.id,
|
||||
@@ -432,7 +425,8 @@ class MailboxRepositoryImpl implements MailboxRepository {
|
||||
);
|
||||
final row = await (_db.select(
|
||||
_db.mailboxes,
|
||||
)..where((t) => t.id.equals(dbId))).getSingle();
|
||||
)..where((t) => t.id.equals(dbId)))
|
||||
.getSingle();
|
||||
return _toModel(row);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,11 +10,10 @@ class SearchHistoryRepositoryImpl implements SearchHistoryRepository {
|
||||
|
||||
@override
|
||||
Future<List<String>> getRecentSearches() async {
|
||||
final rows =
|
||||
await (_db.select(_db.searchHistoryEntries)
|
||||
..orderBy([(t) => OrderingTerm.desc(t.searchedAt)])
|
||||
..limit(_maxEntries))
|
||||
.get();
|
||||
final rows = await (_db.select(_db.searchHistoryEntries)
|
||||
..orderBy([(t) => OrderingTerm.desc(t.searchedAt)])
|
||||
..limit(_maxEntries))
|
||||
.get();
|
||||
return rows.map((r) => r.query).toList();
|
||||
}
|
||||
|
||||
@@ -27,11 +26,10 @@ class SearchHistoryRepositoryImpl implements SearchHistoryRepository {
|
||||
// Remove existing entry for same query (deduplication).
|
||||
await (_db.delete(
|
||||
_db.searchHistoryEntries,
|
||||
)..where((t) => t.query.equals(trimmed))).go();
|
||||
)..where((t) => t.query.equals(trimmed)))
|
||||
.go();
|
||||
|
||||
await _db
|
||||
.into(_db.searchHistoryEntries)
|
||||
.insert(
|
||||
await _db.into(_db.searchHistoryEntries).insert(
|
||||
SearchHistoryEntriesCompanion.insert(
|
||||
query: trimmed,
|
||||
searchedAt: DateTime.now(),
|
||||
@@ -39,17 +37,17 @@ class SearchHistoryRepositoryImpl implements SearchHistoryRepository {
|
||||
);
|
||||
|
||||
// Prune to the most recent _maxEntries.
|
||||
final keepIds =
|
||||
await (_db.select(_db.searchHistoryEntries)
|
||||
..orderBy([(t) => OrderingTerm.desc(t.searchedAt)])
|
||||
..limit(_maxEntries))
|
||||
.map((r) => r.id)
|
||||
.get();
|
||||
final keepIds = await (_db.select(_db.searchHistoryEntries)
|
||||
..orderBy([(t) => OrderingTerm.desc(t.searchedAt)])
|
||||
..limit(_maxEntries))
|
||||
.map((r) => r.id)
|
||||
.get();
|
||||
|
||||
if (keepIds.isNotEmpty) {
|
||||
await (_db.delete(
|
||||
_db.searchHistoryEntries,
|
||||
)..where((t) => t.id.isNotIn(keepIds))).go();
|
||||
)..where((t) => t.id.isNotIn(keepIds)))
|
||||
.go();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -23,9 +23,7 @@ class ShareKeyRepositoryImpl implements ShareKeyRepository {
|
||||
final keyIdHex = _hex(material.keyId);
|
||||
final expiresAt = DateTime.now().toUtc().add(const Duration(minutes: 20));
|
||||
|
||||
await _db
|
||||
.into(_db.shareKeys)
|
||||
.insert(
|
||||
await _db.into(_db.shareKeys).insert(
|
||||
ShareKeysCompanion.insert(
|
||||
id: keyIdHex,
|
||||
publicKey: base64.encode(material.publicKeyBytes),
|
||||
@@ -44,7 +42,8 @@ class ShareKeyRepositoryImpl implements ShareKeyRepository {
|
||||
final keyIdHex = _hex(keyId);
|
||||
final row = await (_db.select(
|
||||
_db.shareKeys,
|
||||
)..where((t) => t.id.equals(keyIdHex))).getSingleOrNull();
|
||||
)..where((t) => t.id.equals(keyIdHex)))
|
||||
.getSingleOrNull();
|
||||
|
||||
if (row == null) return null;
|
||||
if (row.expiresAt.isBefore(DateTime.now().toUtc())) return null;
|
||||
@@ -58,8 +57,8 @@ class ShareKeyRepositoryImpl implements ShareKeyRepository {
|
||||
|
||||
Future<void> _pruneExpired() async {
|
||||
await (_db.delete(
|
||||
_db.shareKeys,
|
||||
)..where((t) => t.expiresAt.isSmallerThanValue(DateTime.now().toUtc())))
|
||||
_db.shareKeys,
|
||||
)..where((t) => t.expiresAt.isSmallerThanValue(DateTime.now().toUtc())))
|
||||
.go();
|
||||
}
|
||||
|
||||
|
||||
@@ -27,9 +27,7 @@ class SyncLogRepositoryImpl implements SyncLogRepository {
|
||||
String? protocolLog,
|
||||
}) async {
|
||||
await _db.transaction(() async {
|
||||
final logId = await _db
|
||||
.into(_db.syncLogs)
|
||||
.insert(
|
||||
final logId = await _db.into(_db.syncLogs).insert(
|
||||
SyncLogsCompanion.insert(
|
||||
accountId: accountId,
|
||||
result: success ? 'ok' : 'error',
|
||||
@@ -48,9 +46,7 @@ class SyncLogRepositoryImpl implements SyncLogRepository {
|
||||
),
|
||||
);
|
||||
for (final s in mailboxStats) {
|
||||
await _db
|
||||
.into(_db.syncLogMailboxes)
|
||||
.insert(
|
||||
await _db.into(_db.syncLogMailboxes).insert(
|
||||
SyncLogMailboxesCompanion.insert(
|
||||
syncLogId: logId,
|
||||
mailboxPath: s.mailboxPath,
|
||||
@@ -74,11 +70,10 @@ class SyncLogRepositoryImpl implements SyncLogRepository {
|
||||
return logsQuery.watch().asyncMap((rows) async {
|
||||
final entries = <SyncLogEntry>[];
|
||||
for (final r in rows) {
|
||||
final mailboxRows =
|
||||
await (_db.select(_db.syncLogMailboxes)
|
||||
..where((t) => t.syncLogId.equals(r.id))
|
||||
..orderBy([(t) => OrderingTerm.asc(t.mailboxPath)]))
|
||||
.get();
|
||||
final mailboxRows = await (_db.select(_db.syncLogMailboxes)
|
||||
..where((t) => t.syncLogId.equals(r.id))
|
||||
..orderBy([(t) => OrderingTerm.asc(t.mailboxPath)]))
|
||||
.get();
|
||||
entries.add(
|
||||
SyncLogEntry(
|
||||
id: r.id,
|
||||
|
||||
@@ -11,9 +11,7 @@ class UndoRepositoryImpl implements UndoRepository {
|
||||
|
||||
@override
|
||||
Future<void> saveAction(UndoAction action) async {
|
||||
await _db
|
||||
.into(_db.undoActions)
|
||||
.insert(
|
||||
await _db.into(_db.undoActions).insert(
|
||||
UndoActionsCompanion.insert(
|
||||
id: action.id,
|
||||
accountId: action.accountId,
|
||||
@@ -31,11 +29,10 @@ class UndoRepositoryImpl implements UndoRepository {
|
||||
|
||||
@override
|
||||
Future<List<UndoAction>> getHistory({int limit = 10}) async {
|
||||
final rows =
|
||||
await (_db.select(_db.undoActions)
|
||||
..orderBy([(t) => OrderingTerm.desc(t.createdAt)])
|
||||
..limit(limit))
|
||||
.get();
|
||||
final rows = await (_db.select(_db.undoActions)
|
||||
..orderBy([(t) => OrderingTerm.desc(t.createdAt)])
|
||||
..limit(limit))
|
||||
.get();
|
||||
return rows.map((row) {
|
||||
return UndoAction.fromJson(
|
||||
jsonDecode(row.dataJson) as Map<String, dynamic>,
|
||||
|
||||
@@ -13,14 +13,14 @@ class UserPreferencesRepositoryImpl implements UserPreferencesRepository {
|
||||
Stream<pref.UserPreferences> observePreferences() {
|
||||
return (_db.select(
|
||||
_db.userPreferences,
|
||||
)..where((t) => t.id.equals(_rowId))).watchSingleOrNull().map(_rowToModel);
|
||||
)..where((t) => t.id.equals(_rowId)))
|
||||
.watchSingleOrNull()
|
||||
.map(_rowToModel);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateMenuPosition(pref.MenuPosition position) async {
|
||||
await _db
|
||||
.into(_db.userPreferences)
|
||||
.insertOnConflictUpdate(
|
||||
await _db.into(_db.userPreferences).insertOnConflictUpdate(
|
||||
UserPreferencesCompanion(
|
||||
id: const Value(_rowId),
|
||||
menuPosition: Value(position.name),
|
||||
@@ -30,9 +30,7 @@ class UserPreferencesRepositoryImpl implements UserPreferencesRepository {
|
||||
|
||||
@override
|
||||
Future<void> updateMailViewButtonPosition(pref.MenuPosition position) async {
|
||||
await _db
|
||||
.into(_db.userPreferences)
|
||||
.insertOnConflictUpdate(
|
||||
await _db.into(_db.userPreferences).insertOnConflictUpdate(
|
||||
UserPreferencesCompanion(
|
||||
id: const Value(_rowId),
|
||||
mailViewButtonPosition: Value(position.name),
|
||||
@@ -44,9 +42,7 @@ class UserPreferencesRepositoryImpl implements UserPreferencesRepository {
|
||||
Future<void> updateAfterMailViewAction(
|
||||
pref.AfterMailViewAction action,
|
||||
) async {
|
||||
await _db
|
||||
.into(_db.userPreferences)
|
||||
.insertOnConflictUpdate(
|
||||
await _db.into(_db.userPreferences).insertOnConflictUpdate(
|
||||
UserPreferencesCompanion(
|
||||
id: const Value(_rowId),
|
||||
afterMailViewAction: Value(action.name),
|
||||
|
||||
Reference in New Issue
Block a user