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