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:
Thomas SharedInbox
2026-05-12 17:49:41 +02:00
co-authored by Claude Sonnet 4.6
parent 49737355b2
commit 8272b75b34
5 changed files with 185 additions and 144 deletions
+5
View File
@@ -0,0 +1,5 @@
# SharedInbox CI Runner
Installed like explained here:
https://forgejo.org/docs/next/admin/actions/installation/binary/
+12 -5
View File
@@ -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) {
+8 -10
View File
@@ -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})');
+14 -22
View File
@@ -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 {
+146 -107
View File
@@ -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),
),
); );
} }
} }