Files
sharedinbox/lib/data/repositories/draft_repository_impl.dart
T

219 lines
6.9 KiB
Dart

import 'package:drift/drift.dart';
import 'package:enough_mail/enough_mail.dart' as imap;
import 'package:sharedinbox/core/models/account.dart';
import 'package:sharedinbox/core/models/draft.dart';
import 'package:sharedinbox/core/repositories/account_repository.dart';
import 'package:sharedinbox/core/repositories/draft_repository.dart';
import 'package:sharedinbox/data/db/database.dart';
import 'package:sharedinbox/data/imap/imap_client_factory.dart';
class DraftRepositoryImpl implements DraftRepository {
DraftRepositoryImpl(this._db, this._accounts, {ImapConnectFn? imapConnect})
: _imapConnect = imapConnect;
final AppDatabase _db;
final AccountRepository _accounts;
final ImapConnectFn? _imapConnect;
@override
Future<SavedDraft> saveDraft({
int? id,
String? accountId,
String? replyToEmailId,
required String toText,
required String ccText,
required String subjectText,
required String bodyText,
}) async {
final now = DateTime.now();
if (id != null) {
await (_db.update(_db.drafts)..where((t) => t.id.equals(id))).write(
DraftsCompanion(
accountId: Value(accountId),
replyToEmailId: Value(replyToEmailId),
toText: Value(toText),
ccText: Value(ccText),
subjectText: Value(subjectText),
bodyText: Value(bodyText),
updatedAt: Value(now),
),
);
return SavedDraft(
id: id,
accountId: accountId,
replyToEmailId: replyToEmailId,
toText: toText,
ccText: ccText,
subjectText: subjectText,
bodyText: bodyText,
updatedAt: now,
);
}
final newId = await _db.into(_db.drafts).insert(
DraftsCompanion.insert(
accountId: Value(accountId),
replyToEmailId: Value(replyToEmailId),
toText: Value(toText),
ccText: Value(ccText),
subjectText: Value(subjectText),
bodyText: Value(bodyText),
updatedAt: now,
),
);
return SavedDraft(
id: newId,
accountId: accountId,
replyToEmailId: replyToEmailId,
toText: toText,
ccText: ccText,
subjectText: subjectText,
bodyText: bodyText,
updatedAt: now,
);
}
@override
Future<SavedDraft?> findDraft({String? replyToEmailId}) async {
final query = _db.select(_db.drafts);
if (replyToEmailId == null) {
query.where((t) => t.replyToEmailId.isNull());
} else {
query.where((t) => t.replyToEmailId.equals(replyToEmailId));
}
query.orderBy([(t) => OrderingTerm.desc(t.id)]);
query.limit(1);
final row = await query.getSingleOrNull();
return row == null ? null : _toModel(row);
}
@override
Future<SavedDraft?> getDraft(int id) async {
final row = await (_db.select(
_db.drafts,
)..where((t) => t.id.equals(id)))
.getSingleOrNull();
return row == null ? null : _toModel(row);
}
@override
Future<void> deleteDraft(int id) async {
await (_db.delete(_db.drafts)..where((t) => t.id.equals(id))).go();
}
@override
Future<void> syncDrafts(String accountId, String password) async {
final connect = _imapConnect;
if (connect == null) return;
final account = await _accounts.getAccount(accountId);
if (account == null || account.type != AccountType.imap) return;
final username =
account.username.isNotEmpty ? account.username : account.email;
imap.ImapClient? client;
try {
client = await connect(account, username, password);
await _syncWithServer(client, accountId);
} finally {
await client?.logout();
}
}
Future<void> _syncWithServer(imap.ImapClient client, String accountId) async {
// Create/select the Drafts folder.
try {
await client.createMailbox('Drafts');
} catch (_) {
// Already exists.
}
final selectResult = await client.selectMailboxByPath('Drafts');
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();
for (final row in localDrafts) {
final builder = imap.MessageBuilder()
..to = _parseAddresses(row.toText)
..cc = _parseAddresses(row.ccText)
..subject = row.subjectText
..text = row.bodyText;
final mime = builder.buildMimeMessage();
final appendResult = await client.appendMessage(
mime,
targetMailboxPath: 'Drafts',
flags: [r'\Draft'],
);
final uidList =
appendResult.responseCodeAppendUid?.targetSequence.toList();
final uid = (uidList != null && uidList.isNotEmpty)
? uidList.first.toString()
: null;
if (uid != null) {
await (_db.update(_db.drafts)..where((t) => t.id.equals(row.id))).write(
DraftsCompanion(imapServerId: Value(uid)),
);
}
}
// 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 knownIds = knownServerIds.map((r) => r.imapServerId!).toSet();
final seq = imap.MessageSequence.fromAll();
final fetch = await client.uidFetchMessages(seq, '(UID FLAGS ENVELOPE)');
for (final msg in fetch.messages) {
final uid = msg.uid?.toString();
if (uid == null || knownIds.contains(uid)) continue;
if (msg.flags?.contains(r'\Deleted') ?? false) continue;
final env = msg.envelope;
final now = DateTime.now();
await _db.into(_db.drafts).insert(
DraftsCompanion.insert(
accountId: Value(accountId),
toText: Value(_addressListToText(env?.to)),
ccText: Value(_addressListToText(env?.cc)),
subjectText: Value(env?.subject ?? ''),
bodyText: const Value(''),
updatedAt: now,
imapServerId: Value(uid),
),
);
}
}
}
List<imap.MailAddress> _parseAddresses(String text) {
if (text.trim().isEmpty) return [];
return text.split(',').map((s) => imap.MailAddress('', s.trim())).toList();
}
String _addressListToText(List<imap.MailAddress>? addresses) {
if (addresses == null || addresses.isEmpty) return '';
return addresses.map((a) => a.email).join(', ');
}
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,
);
}