fix: add required trailing commas to satisfy require_trailing_commas lint rule
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
49737355b2
commit
8272b75b34
@@ -0,0 +1,5 @@
|
|||||||
|
# SharedInbox CI Runner
|
||||||
|
|
||||||
|
Installed like explained here:
|
||||||
|
|
||||||
|
https://forgejo.org/docs/next/admin/actions/installation/binary/
|
||||||
@@ -111,7 +111,9 @@ class ConnectionTestServiceImpl implements ConnectionTestService {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await client.quit();
|
await client.quit();
|
||||||
} catch (_) {/* best-effort */}
|
} catch (_) {
|
||||||
|
/* best-effort */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _testManageSieve(
|
Future<void> _testManageSieve(
|
||||||
@@ -137,12 +139,16 @@ class ConnectionTestServiceImpl implements ConnectionTestService {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
try {
|
try {
|
||||||
await client.logout();
|
await client.logout();
|
||||||
} catch (_) {/* best-effort */}
|
} catch (_) {
|
||||||
|
/* best-effort */
|
||||||
|
}
|
||||||
throw Exception('ManageSieve: $e');
|
throw Exception('ManageSieve: $e');
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await client.logout();
|
await client.logout();
|
||||||
} catch (_) {/* best-effort */}
|
} catch (_) {
|
||||||
|
/* best-effort */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> _testJmap(Account account, String password) async {
|
Future<String> _testJmap(Account account, String password) async {
|
||||||
@@ -163,8 +169,9 @@ class ConnectionTestServiceImpl implements ConnectionTestService {
|
|||||||
},
|
},
|
||||||
).timeout(const Duration(seconds: 10));
|
).timeout(const Duration(seconds: 10));
|
||||||
if (resp.statusCode == 401 || resp.statusCode == 403) {
|
if (resp.statusCode == 401 || resp.statusCode == 403) {
|
||||||
lastError =
|
lastError = Exception(
|
||||||
Exception('Authentication failed: wrong username or password');
|
'Authentication failed: wrong username or password',
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (resp.statusCode != 200) {
|
if (resp.statusCode != 200) {
|
||||||
|
|||||||
@@ -7,10 +7,7 @@ import 'package:http/http.dart' as http;
|
|||||||
import 'package:sharedinbox/data/imap/imap_client_factory.dart'
|
import 'package:sharedinbox/data/imap/imap_client_factory.dart'
|
||||||
show verboseLogKey;
|
show verboseLogKey;
|
||||||
|
|
||||||
const _coreUsing = [
|
const _coreUsing = ['urn:ietf:params:jmap:core', 'urn:ietf:params:jmap:mail'];
|
||||||
'urn:ietf:params:jmap:core',
|
|
||||||
'urn:ietf:params:jmap:mail',
|
|
||||||
];
|
|
||||||
|
|
||||||
const _submissionCapability = 'urn:ietf:params:jmap:submission';
|
const _submissionCapability = 'urn:ietf:params:jmap:submission';
|
||||||
const _sieveCapability = 'urn:ietf:params:jmap:sieve';
|
const _sieveCapability = 'urn:ietf:params:jmap:sieve';
|
||||||
@@ -72,7 +69,9 @@ class JmapClient {
|
|||||||
while (true) {
|
while (true) {
|
||||||
resp = await httpClient.get(
|
resp = await httpClient.get(
|
||||||
jmapUrl,
|
jmapUrl,
|
||||||
headers: {'Authorization': 'Basic $credentials'},
|
headers: {
|
||||||
|
'Authorization': 'Basic $credentials',
|
||||||
|
},
|
||||||
).timeout(const Duration(seconds: 10));
|
).timeout(const Duration(seconds: 10));
|
||||||
if (resp.statusCode != 429 || attempt >= 4) {
|
if (resp.statusCode != 429 || attempt >= 4) {
|
||||||
break;
|
break;
|
||||||
@@ -135,10 +134,7 @@ class JmapClient {
|
|||||||
if (withSubmission) _submissionCapability,
|
if (withSubmission) _submissionCapability,
|
||||||
if (withSieve) _sieveCapability,
|
if (withSieve) _sieveCapability,
|
||||||
];
|
];
|
||||||
final body = jsonEncode({
|
final body = jsonEncode({'using': using, 'methodCalls': methodCalls});
|
||||||
'using': using,
|
|
||||||
'methodCalls': methodCalls,
|
|
||||||
});
|
|
||||||
|
|
||||||
final resp = await _httpClient
|
final resp = await _httpClient
|
||||||
.post(
|
.post(
|
||||||
@@ -224,7 +220,9 @@ class JmapClient {
|
|||||||
);
|
);
|
||||||
final resp = await _httpClient.get(
|
final resp = await _httpClient.get(
|
||||||
url,
|
url,
|
||||||
headers: {'Authorization': 'Basic $_credentials'},
|
headers: {
|
||||||
|
'Authorization': 'Basic $_credentials',
|
||||||
|
},
|
||||||
).timeout(const Duration(seconds: 30));
|
).timeout(const Duration(seconds: 30));
|
||||||
if (resp.statusCode != 200) {
|
if (resp.statusCode != 200) {
|
||||||
throw JmapException('Blob download failed (HTTP ${resp.statusCode})');
|
throw JmapException('Blob download failed (HTTP ${resp.statusCode})');
|
||||||
|
|||||||
@@ -36,22 +36,19 @@ class SieveRepository {
|
|||||||
Future<List<SieveScript>> listScripts(String accountId) async {
|
Future<List<SieveScript>> listScripts(String accountId) async {
|
||||||
final account = await _requireAccount(accountId);
|
final account = await _requireAccount(accountId);
|
||||||
if (account.type == AccountType.imap) {
|
if (account.type == AccountType.imap) {
|
||||||
return _withManageSieve(
|
return _withManageSieve(account, (c) async {
|
||||||
account,
|
final scripts = await c.listScripts();
|
||||||
(c) async {
|
return scripts
|
||||||
final scripts = await c.listScripts();
|
.map(
|
||||||
return scripts
|
(s) => SieveScript(
|
||||||
.map(
|
id: s.name,
|
||||||
(s) => SieveScript(
|
name: s.name,
|
||||||
id: s.name,
|
blobId: s.name,
|
||||||
name: s.name,
|
isActive: s.isActive,
|
||||||
blobId: s.name,
|
),
|
||||||
isActive: s.isActive,
|
)
|
||||||
),
|
.toList();
|
||||||
)
|
});
|
||||||
.toList();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return _withJmap(account, (jmap) async {
|
return _withJmap(account, (jmap) async {
|
||||||
final responses = await jmap.call(
|
final responses = await jmap.call(
|
||||||
@@ -108,12 +105,7 @@ class SieveRepository {
|
|||||||
if (id != null && id != name) {
|
if (id != null && id != name) {
|
||||||
await c.deleteScript(id);
|
await c.deleteScript(id);
|
||||||
}
|
}
|
||||||
return SieveScript(
|
return SieveScript(id: name, name: name, blobId: name, isActive: false);
|
||||||
id: name,
|
|
||||||
name: name,
|
|
||||||
blobId: name,
|
|
||||||
isActive: false,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return _withJmap(account, (jmap) async {
|
return _withJmap(account, (jmap) async {
|
||||||
|
|||||||
@@ -176,16 +176,18 @@ class EmailRepositoryImpl implements EmailRepository {
|
|||||||
participantsJson: Value(jsonEncode(participants)),
|
participantsJson: Value(jsonEncode(participants)),
|
||||||
preview: Value(latest.preview),
|
preview: Value(latest.preview),
|
||||||
latestEmailId: latest.id,
|
latestEmailId: latest.id,
|
||||||
emailIdsJson:
|
emailIdsJson: Value(
|
||||||
Value(jsonEncode(threadEmails.map((e) => e.id).toList())),
|
jsonEncode(threadEmails.map((e) => e.id).toList()),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<model.Email?> getEmail(String emailId) async {
|
Future<model.Email?> getEmail(String emailId) async {
|
||||||
final row = await (_db.select(_db.emails)
|
final row = await (_db.select(
|
||||||
..where((t) => t.id.equals(emailId)))
|
_db.emails,
|
||||||
|
)..where((t) => t.id.equals(emailId)))
|
||||||
.getSingleOrNull();
|
.getSingleOrNull();
|
||||||
return row == null ? null : _toModel(row);
|
return row == null ? null : _toModel(row);
|
||||||
}
|
}
|
||||||
@@ -196,8 +198,9 @@ class EmailRepositoryImpl implements EmailRepository {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<model.EmailBody> getEmailBody(String emailId) async {
|
Future<model.EmailBody> getEmailBody(String emailId) async {
|
||||||
final cached = await (_db.select(_db.emailBodies)
|
final cached = await (_db.select(
|
||||||
..where((t) => t.emailId.equals(emailId)))
|
_db.emailBodies,
|
||||||
|
)..where((t) => t.emailId.equals(emailId)))
|
||||||
.getSingleOrNull();
|
.getSingleOrNull();
|
||||||
if (cached != null) {
|
if (cached != null) {
|
||||||
// Re-fetch if cachedAt is null (legacy row) or older than the TTL.
|
// Re-fetch if cachedAt is null (legacy row) or older than the TTL.
|
||||||
@@ -207,8 +210,9 @@ class EmailRepositoryImpl implements EmailRepository {
|
|||||||
if (age <= _bodyCacheTtl) return _bodyRowToModel(cached);
|
if (age <= _bodyCacheTtl) return _bodyRowToModel(cached);
|
||||||
}
|
}
|
||||||
|
|
||||||
final emailRow = await (_db.select(_db.emails)
|
final emailRow = await (_db.select(
|
||||||
..where((t) => t.id.equals(emailId)))
|
_db.emails,
|
||||||
|
)..where((t) => t.id.equals(emailId)))
|
||||||
.getSingle();
|
.getSingle();
|
||||||
final account = (await _accounts.getAccount(emailRow.accountId))!;
|
final account = (await _accounts.getAccount(emailRow.accountId))!;
|
||||||
final password = await _accounts.getPassword(account.id);
|
final password = await _accounts.getPassword(account.id);
|
||||||
@@ -217,8 +221,11 @@ class EmailRepositoryImpl implements EmailRepository {
|
|||||||
return _getEmailBodyJmap(emailId, account, password);
|
return _getEmailBodyJmap(emailId, account, password);
|
||||||
}
|
}
|
||||||
|
|
||||||
final client =
|
final client = await _imapConnect(
|
||||||
await _imapConnect(account, _effectiveUsername(account), password);
|
account,
|
||||||
|
_effectiveUsername(account),
|
||||||
|
password,
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
await client.selectMailboxByPath(emailRow.mailboxPath);
|
await client.selectMailboxByPath(emailRow.mailboxPath);
|
||||||
final fetch = await client.uidFetchMessage(emailRow.uid, '(BODY.PEEK[])');
|
final fetch = await client.uidFetchMessage(emailRow.uid, '(BODY.PEEK[])');
|
||||||
@@ -364,8 +371,11 @@ class EmailRepositoryImpl implements EmailRepository {
|
|||||||
String password,
|
String password,
|
||||||
String mailboxPath,
|
String mailboxPath,
|
||||||
) async {
|
) async {
|
||||||
final client =
|
final client = await _imapConnect(
|
||||||
await _imapConnect(account, _effectiveUsername(account), password);
|
account,
|
||||||
|
_effectiveUsername(account),
|
||||||
|
password,
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
// Only request CONDSTORE if the server advertises it. Servers that don't
|
// Only request CONDSTORE if the server advertises it. Servers that don't
|
||||||
// support the extension may reject SELECT with (CONDSTORE) with BAD.
|
// support the extension may reject SELECT with (CONDSTORE) with BAD.
|
||||||
@@ -394,7 +404,9 @@ class EmailRepositoryImpl implements EmailRepository {
|
|||||||
}
|
}
|
||||||
// Use UID SEARCH ALL + UID FETCH so every message gets a reliable UID.
|
// Use UID SEARCH ALL + UID FETCH so every message gets a reliable UID.
|
||||||
// Regular FETCH 1:* may not populate msg.uid on all servers.
|
// Regular FETCH 1:* may not populate msg.uid on all servers.
|
||||||
final allUids = (await client.uidSearchMessages(searchCriteria: 'ALL'))
|
final allUids = (await client.uidSearchMessages(
|
||||||
|
searchCriteria: 'ALL',
|
||||||
|
))
|
||||||
.matchingSequence
|
.matchingSequence
|
||||||
?.toList() ??
|
?.toList() ??
|
||||||
[];
|
[];
|
||||||
@@ -454,11 +466,12 @@ class EmailRepositoryImpl implements EmailRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Detect remote deletions.
|
// Detect remote deletions.
|
||||||
final serverUids =
|
final serverUids = (await client.uidSearchMessages(
|
||||||
(await client.uidSearchMessages(searchCriteria: 'ALL'))
|
searchCriteria: 'ALL',
|
||||||
.matchingSequence
|
))
|
||||||
?.toList() ??
|
.matchingSequence
|
||||||
[];
|
?.toList() ??
|
||||||
|
[];
|
||||||
await _reconcileDeletedImap(account.id, mailboxPath, serverUids);
|
await _reconcileDeletedImap(account.id, mailboxPath, serverUids);
|
||||||
final maxUid =
|
final maxUid =
|
||||||
serverUids.isEmpty ? lastUid : serverUids.reduce(math.max);
|
serverUids.isEmpty ? lastUid : serverUids.reduce(math.max);
|
||||||
@@ -519,15 +532,19 @@ class EmailRepositoryImpl implements EmailRepository {
|
|||||||
final fetch = sequence.isUidSequence
|
final fetch = sequence.isUidSequence
|
||||||
? await client.uidFetchMessages(sequence, fetchItems)
|
? await client.uidFetchMessages(sequence, fetchItems)
|
||||||
: await client.fetchMessages(sequence, fetchItems);
|
: await client.fetchMessages(sequence, fetchItems);
|
||||||
final pendingByUid =
|
final pendingByUid = await _pendingDeleteOrMoveUids(
|
||||||
await _pendingDeleteOrMoveUids(account.id, mailboxPath);
|
account.id,
|
||||||
|
mailboxPath,
|
||||||
|
);
|
||||||
var bytes = 0;
|
var bytes = 0;
|
||||||
final affectedThreads = <String>{};
|
final affectedThreads = <String>{};
|
||||||
await _db.transaction(() async {
|
await _db.transaction(() async {
|
||||||
for (final msg in fetch.messages) {
|
for (final msg in fetch.messages) {
|
||||||
final envelope = msg.envelope;
|
final envelope = msg.envelope;
|
||||||
if (envelope == null) {
|
if (envelope == null) {
|
||||||
log('IMAP: skipping message with no envelope (uid=${msg.uid}, mailbox=$mailboxPath)');
|
log(
|
||||||
|
'IMAP: skipping message with no envelope (uid=${msg.uid}, mailbox=$mailboxPath)',
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
final uid = msg.uid;
|
final uid = msg.uid;
|
||||||
@@ -718,11 +735,16 @@ class EmailRepositoryImpl implements EmailRepository {
|
|||||||
String password,
|
String password,
|
||||||
String mailboxPath,
|
String mailboxPath,
|
||||||
) async {
|
) async {
|
||||||
final client =
|
final client = await _imapConnect(
|
||||||
await _imapConnect(account, _effectiveUsername(account), password);
|
account,
|
||||||
|
_effectiveUsername(account),
|
||||||
|
password,
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
await client.selectMailboxByPath(mailboxPath);
|
await client.selectMailboxByPath(mailboxPath);
|
||||||
final serverUids = (await client.uidSearchMessages(searchCriteria: 'ALL'))
|
final serverUids = (await client.uidSearchMessages(
|
||||||
|
searchCriteria: 'ALL',
|
||||||
|
))
|
||||||
.matchingSequence
|
.matchingSequence
|
||||||
?.toList() ??
|
?.toList() ??
|
||||||
[];
|
[];
|
||||||
@@ -818,7 +840,7 @@ class EmailRepositoryImpl implements EmailRepository {
|
|||||||
'position': position,
|
'position': position,
|
||||||
},
|
},
|
||||||
'0',
|
'0',
|
||||||
]
|
],
|
||||||
]);
|
]);
|
||||||
final queryResult = _responseArgs(responses, 0, 'Email/query');
|
final queryResult = _responseArgs(responses, 0, 'Email/query');
|
||||||
final ids = List<String>.from(queryResult['ids'] as List);
|
final ids = List<String>.from(queryResult['ids'] as List);
|
||||||
@@ -863,7 +885,7 @@ class EmailRepositoryImpl implements EmailRepository {
|
|||||||
'properties': ['id', 'keywords'],
|
'properties': ['id', 'keywords'],
|
||||||
},
|
},
|
||||||
'0',
|
'0',
|
||||||
]
|
],
|
||||||
]);
|
]);
|
||||||
final getResult = _responseArgs(responses, 0, 'Email/get');
|
final getResult = _responseArgs(responses, 0, 'Email/get');
|
||||||
final list = getResult['list'] as List<dynamic>;
|
final list = getResult['list'] as List<dynamic>;
|
||||||
@@ -1031,7 +1053,7 @@ class EmailRepositoryImpl implements EmailRepository {
|
|||||||
'Email/changes',
|
'Email/changes',
|
||||||
{'accountId': jmap.accountId, 'sinceState': sinceState},
|
{'accountId': jmap.accountId, 'sinceState': sinceState},
|
||||||
'0',
|
'0',
|
||||||
]
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
final changes = _responseArgs(responses, 0, 'Email/changes');
|
final changes = _responseArgs(responses, 0, 'Email/changes');
|
||||||
@@ -1054,7 +1076,7 @@ class EmailRepositoryImpl implements EmailRepository {
|
|||||||
..._emailGetBodyOptions,
|
..._emailGetBodyOptions,
|
||||||
},
|
},
|
||||||
'1',
|
'1',
|
||||||
]
|
],
|
||||||
]);
|
]);
|
||||||
final getResult = _responseArgs(getResponses, 0, 'Email/get');
|
final getResult = _responseArgs(getResponses, 0, 'Email/get');
|
||||||
final list = getResult['list'] as List<dynamic>;
|
final list = getResult['list'] as List<dynamic>;
|
||||||
@@ -1120,12 +1142,15 @@ class EmailRepositoryImpl implements EmailRepository {
|
|||||||
affectedByMailbox.putIfAbsent(mailboxPath, () => {}).add(jmapThreadId);
|
affectedByMailbox.putIfAbsent(mailboxPath, () => {}).add(jmapThreadId);
|
||||||
|
|
||||||
// JMAP messageId/inReplyTo/references are arrays; join to space-separated.
|
// JMAP messageId/inReplyTo/references are arrays; join to space-separated.
|
||||||
final jmapMessageId =
|
final jmapMessageId = _joinJmapStringList(
|
||||||
_joinJmapStringList(m['messageId'] as List<dynamic>?);
|
m['messageId'] as List<dynamic>?,
|
||||||
final jmapInReplyTo =
|
);
|
||||||
_joinJmapStringList(m['inReplyTo'] as List<dynamic>?);
|
final jmapInReplyTo = _joinJmapStringList(
|
||||||
final jmapReferences =
|
m['inReplyTo'] as List<dynamic>?,
|
||||||
_joinJmapStringList(m['references'] as List<dynamic>?);
|
);
|
||||||
|
final jmapReferences = _joinJmapStringList(
|
||||||
|
m['references'] as List<dynamic>?,
|
||||||
|
);
|
||||||
|
|
||||||
await _db.into(_db.emails).insertOnConflictUpdate(
|
await _db.into(_db.emails).insertOnConflictUpdate(
|
||||||
EmailsCompanion.insert(
|
EmailsCompanion.insert(
|
||||||
@@ -1227,10 +1252,14 @@ class EmailRepositoryImpl implements EmailRepository {
|
|||||||
Future<void> _recordChangeError(PendingChangeRow row, Object error) async {
|
Future<void> _recordChangeError(PendingChangeRow row, Object error) async {
|
||||||
final next = row.attempts + 1;
|
final next = row.attempts + 1;
|
||||||
if (next >= _maxChangeAttempts) {
|
if (next >= _maxChangeAttempts) {
|
||||||
await (_db.delete(_db.pendingChanges)..where((t) => t.id.equals(row.id)))
|
await (_db.delete(
|
||||||
|
_db.pendingChanges,
|
||||||
|
)..where((t) => t.id.equals(row.id)))
|
||||||
.go();
|
.go();
|
||||||
} else {
|
} else {
|
||||||
await (_db.update(_db.pendingChanges)..where((t) => t.id.equals(row.id)))
|
await (_db.update(
|
||||||
|
_db.pendingChanges,
|
||||||
|
)..where((t) => t.id.equals(row.id)))
|
||||||
.write(
|
.write(
|
||||||
PendingChangesCompanion(
|
PendingChangesCompanion(
|
||||||
attempts: Value(next),
|
attempts: Value(next),
|
||||||
@@ -1311,8 +1340,9 @@ class EmailRepositoryImpl implements EmailRepository {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final credentials = base64
|
final credentials = base64.encode(
|
||||||
.encode(utf8.encode('${_effectiveUsername(account)}:$password'));
|
utf8.encode('${_effectiveUsername(account)}:$password'),
|
||||||
|
);
|
||||||
|
|
||||||
http.StreamedResponse response;
|
http.StreamedResponse response;
|
||||||
try {
|
try {
|
||||||
@@ -1404,13 +1434,10 @@ class EmailRepositoryImpl implements EmailRepository {
|
|||||||
// ── Mutations ──────────────────────────────────────────────────────────────
|
// ── Mutations ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> setFlag(
|
Future<void> setFlag(String emailId, {bool? seen, bool? flagged}) async {
|
||||||
String emailId, {
|
final row = await (_db.select(
|
||||||
bool? seen,
|
_db.emails,
|
||||||
bool? flagged,
|
)..where((t) => t.id.equals(emailId)))
|
||||||
}) async {
|
|
||||||
final row = await (_db.select(_db.emails)
|
|
||||||
..where((t) => t.id.equals(emailId)))
|
|
||||||
.getSingle();
|
.getSingle();
|
||||||
final account = (await _accounts.getAccount(row.accountId))!;
|
final account = (await _accounts.getAccount(row.accountId))!;
|
||||||
|
|
||||||
@@ -1451,9 +1478,11 @@ class EmailRepositoryImpl implements EmailRepository {
|
|||||||
account.id,
|
account.id,
|
||||||
emailId,
|
emailId,
|
||||||
'flag_seen',
|
'flag_seen',
|
||||||
jsonEncode(
|
jsonEncode({
|
||||||
{'uid': row.uid, 'mailboxPath': row.mailboxPath, 'seen': seen},
|
'uid': row.uid,
|
||||||
),
|
'mailboxPath': row.mailboxPath,
|
||||||
|
'seen': seen,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (flagged != null) {
|
if (flagged != null) {
|
||||||
@@ -1483,8 +1512,9 @@ class EmailRepositoryImpl implements EmailRepository {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> moveEmail(String emailId, String destMailboxPath) async {
|
Future<void> moveEmail(String emailId, String destMailboxPath) async {
|
||||||
final row = await (_db.select(_db.emails)
|
final row = await (_db.select(
|
||||||
..where((t) => t.id.equals(emailId)))
|
_db.emails,
|
||||||
|
)..where((t) => t.id.equals(emailId)))
|
||||||
.getSingle();
|
.getSingle();
|
||||||
final account = (await _accounts.getAccount(row.accountId))!;
|
final account = (await _accounts.getAccount(row.accountId))!;
|
||||||
|
|
||||||
@@ -1550,8 +1580,9 @@ class EmailRepositoryImpl implements EmailRepository {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String?> deleteEmail(String emailId) async {
|
Future<String?> deleteEmail(String emailId) async {
|
||||||
final row = await (_db.select(_db.emails)
|
final row = await (_db.select(
|
||||||
..where((t) => t.id.equals(emailId)))
|
_db.emails,
|
||||||
|
)..where((t) => t.id.equals(emailId)))
|
||||||
.getSingle();
|
.getSingle();
|
||||||
final account = (await _accounts.getAccount(row.accountId))!;
|
final account = (await _accounts.getAccount(row.accountId))!;
|
||||||
|
|
||||||
@@ -1637,8 +1668,9 @@ class EmailRepositoryImpl implements EmailRepository {
|
|||||||
|
|
||||||
final row = await query.getSingleOrNull();
|
final row = await query.getSingleOrNull();
|
||||||
if (row != null) {
|
if (row != null) {
|
||||||
final count = await (_db.delete(_db.pendingChanges)
|
final count = await (_db.delete(
|
||||||
..where((t) => t.id.equals(row.id)))
|
_db.pendingChanges,
|
||||||
|
)..where((t) => t.id.equals(row.id)))
|
||||||
.go();
|
.go();
|
||||||
return count > 0;
|
return count > 0;
|
||||||
}
|
}
|
||||||
@@ -1647,8 +1679,9 @@ class EmailRepositoryImpl implements EmailRepository {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> snoozeEmail(String emailId, DateTime until) async {
|
Future<void> snoozeEmail(String emailId, DateTime until) async {
|
||||||
final row = await (_db.select(_db.emails)
|
final row = await (_db.select(
|
||||||
..where((t) => t.id.equals(emailId)))
|
_db.emails,
|
||||||
|
)..where((t) => t.id.equals(emailId)))
|
||||||
.getSingle();
|
.getSingle();
|
||||||
final account = (await _accounts.getAccount(row.accountId))!;
|
final account = (await _accounts.getAccount(row.accountId))!;
|
||||||
|
|
||||||
@@ -1696,11 +1729,7 @@ class EmailRepositoryImpl implements EmailRepository {
|
|||||||
row.mailboxPath,
|
row.mailboxPath,
|
||||||
row.threadId ?? emailId,
|
row.threadId ?? emailId,
|
||||||
);
|
);
|
||||||
await _updateThread(
|
await _updateThread(row.accountId, destPath, row.threadId ?? emailId);
|
||||||
row.accountId,
|
|
||||||
destPath,
|
|
||||||
row.threadId ?? emailId,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -1730,11 +1759,7 @@ class EmailRepositoryImpl implements EmailRepository {
|
|||||||
accountId,
|
accountId,
|
||||||
row.id,
|
row.id,
|
||||||
'unsnooze',
|
'unsnooze',
|
||||||
jsonEncode({
|
jsonEncode({'uid': row.uid, 'src': row.mailboxPath, 'dest': dest}),
|
||||||
'uid': row.uid,
|
|
||||||
'src': row.mailboxPath,
|
|
||||||
'dest': dest,
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Optimistic local update.
|
// Optimistic local update.
|
||||||
@@ -1822,10 +1847,14 @@ class EmailRepositoryImpl implements EmailRepository {
|
|||||||
|
|
||||||
for (final row in rows) {
|
for (final row in rows) {
|
||||||
try {
|
try {
|
||||||
final newState =
|
final newState = await _applyPendingChangeJmap(
|
||||||
await _applyPendingChangeJmap(jmap, row, ifInState: ifInState);
|
jmap,
|
||||||
await (_db.delete(_db.pendingChanges)
|
row,
|
||||||
..where((t) => t.id.equals(row.id)))
|
ifInState: ifInState,
|
||||||
|
);
|
||||||
|
await (_db.delete(
|
||||||
|
_db.pendingChanges,
|
||||||
|
)..where((t) => t.id.equals(row.id)))
|
||||||
.go();
|
.go();
|
||||||
applied++;
|
applied++;
|
||||||
// Keep our checkpoint in sync with whatever the server returned.
|
// Keep our checkpoint in sync with whatever the server returned.
|
||||||
@@ -1852,8 +1881,9 @@ class EmailRepositoryImpl implements EmailRepository {
|
|||||||
} on JmapSetItemException catch (e) {
|
} on JmapSetItemException catch (e) {
|
||||||
// Permanent per-item rejection (e.g. notFound, forbidden) — discard
|
// Permanent per-item rejection (e.g. notFound, forbidden) — discard
|
||||||
// the change so the queue doesn't grow unboundedly.
|
// the change so the queue doesn't grow unboundedly.
|
||||||
await (_db.delete(_db.pendingChanges)
|
await (_db.delete(
|
||||||
..where((t) => t.id.equals(row.id)))
|
_db.pendingChanges,
|
||||||
|
)..where((t) => t.id.equals(row.id)))
|
||||||
.go();
|
.go();
|
||||||
log('JMAP permanent error for change ${row.id}: $e');
|
log('JMAP permanent error for change ${row.id}: $e');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -1870,8 +1900,11 @@ class EmailRepositoryImpl implements EmailRepository {
|
|||||||
) async {
|
) async {
|
||||||
imap.ImapClient? client;
|
imap.ImapClient? client;
|
||||||
try {
|
try {
|
||||||
client =
|
client = await _imapConnect(
|
||||||
await _imapConnect(account, _effectiveUsername(account), password);
|
account,
|
||||||
|
_effectiveUsername(account),
|
||||||
|
password,
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Connection-level failure — bump all rows, they'll retry next cycle.
|
// Connection-level failure — bump all rows, they'll retry next cycle.
|
||||||
for (final row in rows) {
|
for (final row in rows) {
|
||||||
@@ -1884,8 +1917,9 @@ class EmailRepositoryImpl implements EmailRepository {
|
|||||||
for (final row in rows) {
|
for (final row in rows) {
|
||||||
try {
|
try {
|
||||||
await _applyPendingChangeImap(client, row);
|
await _applyPendingChangeImap(client, row);
|
||||||
await (_db.delete(_db.pendingChanges)
|
await (_db.delete(
|
||||||
..where((t) => t.id.equals(row.id)))
|
_db.pendingChanges,
|
||||||
|
)..where((t) => t.id.equals(row.id)))
|
||||||
.go();
|
.go();
|
||||||
applied++;
|
applied++;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -1993,7 +2027,7 @@ class EmailRepositoryImpl implements EmailRepository {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
'0',
|
'0',
|
||||||
]
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
case 'flag_flagged':
|
case 'flag_flagged':
|
||||||
@@ -2007,7 +2041,7 @@ class EmailRepositoryImpl implements EmailRepository {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
'0',
|
'0',
|
||||||
]
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
case 'move':
|
case 'move':
|
||||||
@@ -2025,7 +2059,7 @@ class EmailRepositoryImpl implements EmailRepository {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
'0',
|
'0',
|
||||||
]
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
case 'delete':
|
case 'delete':
|
||||||
@@ -2036,7 +2070,7 @@ class EmailRepositoryImpl implements EmailRepository {
|
|||||||
'destroy': [jmapEmailId],
|
'destroy': [jmapEmailId],
|
||||||
}),
|
}),
|
||||||
'0',
|
'0',
|
||||||
]
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
case 'snooze':
|
case 'snooze':
|
||||||
@@ -2058,7 +2092,7 @@ class EmailRepositoryImpl implements EmailRepository {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
'0',
|
'0',
|
||||||
]
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
case 'unsnooze':
|
case 'unsnooze':
|
||||||
@@ -2074,7 +2108,7 @@ class EmailRepositoryImpl implements EmailRepository {
|
|||||||
'properties': ['keywords'],
|
'properties': ['keywords'],
|
||||||
},
|
},
|
||||||
'0',
|
'0',
|
||||||
]
|
],
|
||||||
]);
|
]);
|
||||||
final getResult = _responseArgs(getResponses, 0, 'Email/get');
|
final getResult = _responseArgs(getResponses, 0, 'Email/get');
|
||||||
final email = (getResult['list'] as List).firstOrNull as Map?;
|
final email = (getResult['list'] as List).firstOrNull as Map?;
|
||||||
@@ -2098,7 +2132,7 @@ class EmailRepositoryImpl implements EmailRepository {
|
|||||||
'update': {jmapEmailId: update},
|
'update': {jmapEmailId: update},
|
||||||
}),
|
}),
|
||||||
'0',
|
'0',
|
||||||
]
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -2163,8 +2197,11 @@ class EmailRepositoryImpl implements EmailRepository {
|
|||||||
await builder.addFile(file, mediaType);
|
await builder.addFile(file, mediaType);
|
||||||
}
|
}
|
||||||
final mimeMessage = builder.buildMimeMessage();
|
final mimeMessage = builder.buildMimeMessage();
|
||||||
final smtpClient =
|
final smtpClient = await _smtpConnect(
|
||||||
await _smtpConnect(account, _effectiveUsername(account), password);
|
account,
|
||||||
|
_effectiveUsername(account),
|
||||||
|
password,
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
await smtpClient.sendMessage(mimeMessage);
|
await smtpClient.sendMessage(mimeMessage);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -2172,8 +2209,11 @@ class EmailRepositoryImpl implements EmailRepository {
|
|||||||
}
|
}
|
||||||
// Save a copy to the Sent folder via IMAP APPEND.
|
// Save a copy to the Sent folder via IMAP APPEND.
|
||||||
// Create the folder first — many servers don't pre-create it.
|
// Create the folder first — many servers don't pre-create it.
|
||||||
final imapClient =
|
final imapClient = await _imapConnect(
|
||||||
await _imapConnect(account, _effectiveUsername(account), password);
|
account,
|
||||||
|
_effectiveUsername(account),
|
||||||
|
password,
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
try {
|
try {
|
||||||
await imapClient.createMailbox('Sent');
|
await imapClient.createMailbox('Sent');
|
||||||
@@ -2224,7 +2264,9 @@ class EmailRepositoryImpl implements EmailRepository {
|
|||||||
|
|
||||||
// Look up the Sent mailbox JMAP ID from the local DB.
|
// Look up the Sent mailbox JMAP ID from the local DB.
|
||||||
final sentMailbox = await (_db.select(_db.mailboxes)
|
final sentMailbox = await (_db.select(_db.mailboxes)
|
||||||
..where((t) => t.accountId.equals(account.id) & t.role.equals('sent'))
|
..where(
|
||||||
|
(t) => t.accountId.equals(account.id) & t.role.equals('sent'),
|
||||||
|
)
|
||||||
..limit(1))
|
..limit(1))
|
||||||
.getSingleOrNull();
|
.getSingleOrNull();
|
||||||
final sentJmapId = sentMailbox?.path;
|
final sentJmapId = sentMailbox?.path;
|
||||||
@@ -2370,8 +2412,9 @@ class EmailRepositoryImpl implements EmailRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final emailRow = await (_db.select(_db.emails)
|
final emailRow = await (_db.select(
|
||||||
..where((t) => t.id.equals(emailId)))
|
_db.emails,
|
||||||
|
)..where((t) => t.id.equals(emailId)))
|
||||||
.getSingle();
|
.getSingle();
|
||||||
final account = (await _accounts.getAccount(emailRow.accountId))!;
|
final account = (await _accounts.getAccount(emailRow.accountId))!;
|
||||||
final password = await _accounts.getPassword(account.id);
|
final password = await _accounts.getPassword(account.id);
|
||||||
@@ -2392,8 +2435,11 @@ class EmailRepositoryImpl implements EmailRepository {
|
|||||||
return file.path;
|
return file.path;
|
||||||
}
|
}
|
||||||
|
|
||||||
final client =
|
final client = await _imapConnect(
|
||||||
await _imapConnect(account, _effectiveUsername(account), password);
|
account,
|
||||||
|
_effectiveUsername(account),
|
||||||
|
password,
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
await client.selectMailboxByPath(emailRow.mailboxPath);
|
await client.selectMailboxByPath(emailRow.mailboxPath);
|
||||||
final fetch = await client.uidFetchMessage(
|
final fetch = await client.uidFetchMessage(
|
||||||
@@ -2404,9 +2450,7 @@ class EmailRepositoryImpl implements EmailRepository {
|
|||||||
final part = msg.getPart(attachment.fetchPartId) ?? msg;
|
final part = msg.getPart(attachment.fetchPartId) ?? msg;
|
||||||
final bytes = part.decodeContentBinary();
|
final bytes = part.decodeContentBinary();
|
||||||
if (bytes == null) {
|
if (bytes == null) {
|
||||||
throw StateError(
|
throw StateError('Failed to decode attachment ${attachment.filename}.');
|
||||||
'Failed to decode attachment ${attachment.filename}.',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
await file.writeAsBytes(bytes);
|
await file.writeAsBytes(bytes);
|
||||||
return file.path;
|
return file.path;
|
||||||
@@ -2475,8 +2519,11 @@ class EmailRepositoryImpl implements EmailRepository {
|
|||||||
) async {
|
) async {
|
||||||
final account = (await _accounts.getAccount(accountId))!;
|
final account = (await _accounts.getAccount(accountId))!;
|
||||||
final password = await _accounts.getPassword(accountId);
|
final password = await _accounts.getPassword(accountId);
|
||||||
final client =
|
final client = await _imapConnect(
|
||||||
await _imapConnect(account, _effectiveUsername(account), password);
|
account,
|
||||||
|
_effectiveUsername(account),
|
||||||
|
password,
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
await client.selectMailboxByPath(mailboxPath);
|
await client.selectMailboxByPath(mailboxPath);
|
||||||
final terms =
|
final terms =
|
||||||
@@ -2524,12 +2571,7 @@ class EmailRepositoryImpl implements EmailRepository {
|
|||||||
|
|
||||||
List<model.EmailAddress> _toAddressList(List<imap.MailAddress>? addresses) =>
|
List<model.EmailAddress> _toAddressList(List<imap.MailAddress>? addresses) =>
|
||||||
(addresses ?? const [])
|
(addresses ?? const [])
|
||||||
.map(
|
.map((a) => model.EmailAddress(name: a.personalName, email: a.email))
|
||||||
(a) => model.EmailAddress(
|
|
||||||
name: a.personalName,
|
|
||||||
email: a.email,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
// ── Helpers ────────────────────────────────────────────────────────────────
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||||
@@ -2694,10 +2736,7 @@ class EmailRepositoryImpl implements EmailRepository {
|
|||||||
@override
|
@override
|
||||||
Future<void> retryMutation(int id) async {
|
Future<void> retryMutation(int id) async {
|
||||||
await (_db.update(_db.pendingChanges)..where((t) => t.id.equals(id))).write(
|
await (_db.update(_db.pendingChanges)..where((t) => t.id.equals(id))).write(
|
||||||
const PendingChangesCompanion(
|
const PendingChangesCompanion(attempts: Value(0), lastError: Value(null)),
|
||||||
attempts: Value(0),
|
|
||||||
lastError: Value(null),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user