Fix API mismatches, lint violations, and test failures
- enough_mail: use uidFetchMessage/uidMarkSeen/uidMarkFlagged/uidMove/
uidMarkDeleted/uidExpunge, remove non-existent isUidSequence param,
fix SmtpClient construction and use quit() not disconnect()
- Drift: add @DataClassName('MailboxRow') to avoid ugly 'Mailboxe',
alias core model imports to resolve type name conflicts
- EmailsCompanion.insert: uid/receivedAt are required, not Value<T>
- Lint: remove unrecognised rules (prefer_const_collections,
avoid_returning_null_for_future), add missing mounted guards after await
- Tests: fix html_utils expectations to match trim() behaviour,
add explicit Map casts in email_model_test for avoid_dynamic_calls
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
03d35387f7
commit
4e03483126
+21
-6
@@ -24,14 +24,29 @@ android/.gradle/
|
||||
android/local.properties
|
||||
android/app/google-services.json
|
||||
android/key.properties
|
||||
android/app/src/main/java/io/flutter/plugins/
|
||||
|
||||
# iOS / macOS
|
||||
# iOS
|
||||
ios/Pods/
|
||||
macos/Pods/
|
||||
ios/Flutter/Generated.xcconfig
|
||||
ios/Flutter/ephemeral/
|
||||
ios/Flutter/flutter_export_environment.sh
|
||||
ios/Runner/GeneratedPluginRegistrant.*
|
||||
*.xcworkspace/
|
||||
|
||||
# Linux build output
|
||||
linux/build/
|
||||
# macOS
|
||||
macos/Pods/
|
||||
macos/Flutter/GeneratedPluginRegistrant.swift
|
||||
macos/Flutter/ephemeral/
|
||||
|
||||
# Stalwart dev server runtime data (created by stalwart-dev/start)
|
||||
/tmp/stalwart-dev-*/
|
||||
# Linux generated plugin wiring (recreated by flutter pub get)
|
||||
linux/flutter/ephemeral/
|
||||
linux/flutter/generated_plugin_registrant.*
|
||||
linux/flutter/generated_plugins.cmake
|
||||
|
||||
# Flutter generated metadata
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
|
||||
# direnv cache
|
||||
.direnv/
|
||||
|
||||
@@ -10,6 +10,8 @@ analyzer:
|
||||
- lib/data/db/database.g.dart
|
||||
- "**/*.g.dart"
|
||||
- "**/*.freezed.dart"
|
||||
# Vendored library — analyze only our own code
|
||||
- "packages/**"
|
||||
|
||||
linter:
|
||||
rules:
|
||||
@@ -17,7 +19,6 @@ linter:
|
||||
- prefer_single_quotes
|
||||
- prefer_const_constructors
|
||||
- prefer_const_declarations
|
||||
- prefer_const_collections
|
||||
- prefer_final_locals
|
||||
- prefer_final_in_for_each
|
||||
- unnecessary_const
|
||||
@@ -30,7 +31,6 @@ linter:
|
||||
- avoid_dynamic_calls
|
||||
- avoid_empty_else
|
||||
- avoid_print
|
||||
- avoid_returning_null_for_future
|
||||
- avoid_returning_null_for_void
|
||||
- avoid_type_to_string
|
||||
- cancel_subscriptions
|
||||
|
||||
@@ -24,6 +24,7 @@ class Accounts extends Table {
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
|
||||
@DataClassName('MailboxRow')
|
||||
class Mailboxes extends Table {
|
||||
TextColumn get id => text()();
|
||||
TextColumn get accountId =>
|
||||
|
||||
@@ -5,7 +5,7 @@ import '../../core/utils/logger.dart';
|
||||
|
||||
/// Opens an authenticated IMAP client for [account].
|
||||
Future<ImapClient> connectImap(Account account, String password) async {
|
||||
final client = ImapClient(isLogEnabled: false);
|
||||
final client = ImapClient();
|
||||
await client.connectToServer(
|
||||
account.imapHost,
|
||||
account.imapPort,
|
||||
@@ -25,7 +25,7 @@ Future<SmtpClient> connectSmtp(Account account, String password) async {
|
||||
final clientDomain =
|
||||
atIndex != -1 ? account.email.substring(atIndex + 1) : account.smtpHost;
|
||||
|
||||
final client = SmtpClient(clientDomain, isLogEnabled: false);
|
||||
final client = SmtpClient(clientDomain);
|
||||
await client.connectToServer(
|
||||
account.smtpHost,
|
||||
account.smtpPort,
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
|
||||
import '../../core/models/account.dart';
|
||||
import '../../core/models/account.dart' as model;
|
||||
import '../../core/repositories/account_repository.dart';
|
||||
import '../db/database.dart';
|
||||
import '../db/database.dart' as db show Account;
|
||||
|
||||
class AccountRepositoryImpl implements AccountRepository {
|
||||
AccountRepositoryImpl(this._db);
|
||||
@@ -14,14 +11,14 @@ class AccountRepositoryImpl implements AccountRepository {
|
||||
final _storage = const FlutterSecureStorage();
|
||||
|
||||
@override
|
||||
Stream<List<Account>> observeAccounts() {
|
||||
Stream<List<model.Account>> observeAccounts() {
|
||||
return _db.select(_db.accounts).watch().map(
|
||||
(rows) => rows.map(_toModel).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Account?> getAccount(String id) async {
|
||||
Future<model.Account?> getAccount(String id) async {
|
||||
final row = await (_db.select(_db.accounts)
|
||||
..where((t) => t.id.equals(id)))
|
||||
.getSingleOrNull();
|
||||
@@ -29,7 +26,7 @@ class AccountRepositoryImpl implements AccountRepository {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> addAccount(Account account, String password) async {
|
||||
Future<void> addAccount(model.Account account, String password) async {
|
||||
await _db.into(_db.accounts).insertOnConflictUpdate(
|
||||
AccountsCompanion.insert(
|
||||
id: account.id,
|
||||
@@ -55,13 +52,15 @@ class AccountRepositoryImpl implements AccountRepository {
|
||||
@override
|
||||
Future<String> getPassword(String accountId) async {
|
||||
final pw = await _storage.read(key: _passwordKey(accountId));
|
||||
if (pw == null) throw StateError('No password stored for account $accountId');
|
||||
if (pw == null) {
|
||||
throw StateError('No password stored for account $accountId');
|
||||
}
|
||||
return pw;
|
||||
}
|
||||
|
||||
String _passwordKey(String accountId) => 'account_password_$accountId';
|
||||
|
||||
Account _toModel(db.Account row) => Account(
|
||||
model.Account _toModel(Account row) => model.Account(
|
||||
id: row.id,
|
||||
displayName: row.displayName,
|
||||
email: row.email,
|
||||
|
||||
@@ -3,11 +3,10 @@ import 'dart:convert';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:enough_mail/enough_mail.dart' as imap;
|
||||
|
||||
import '../../core/models/email.dart';
|
||||
import '../../core/models/email.dart' as model;
|
||||
import '../../core/repositories/account_repository.dart';
|
||||
import '../../core/repositories/email_repository.dart';
|
||||
import '../db/database.dart';
|
||||
import '../db/database.dart' as db show Email, EmailBody;
|
||||
import '../imap/imap_client_factory.dart';
|
||||
|
||||
class EmailRepositoryImpl implements EmailRepository {
|
||||
@@ -19,7 +18,10 @@ class EmailRepositoryImpl implements EmailRepository {
|
||||
// ── Observe ────────────────────────────────────────────────────────────────
|
||||
|
||||
@override
|
||||
Stream<List<Email>> observeEmails(String accountId, String mailboxPath) {
|
||||
Stream<List<model.Email>> observeEmails(
|
||||
String accountId,
|
||||
String mailboxPath,
|
||||
) {
|
||||
return (_db.select(_db.emails)
|
||||
..where(
|
||||
(t) =>
|
||||
@@ -32,7 +34,7 @@ class EmailRepositoryImpl implements EmailRepository {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Email?> getEmail(String emailId) async {
|
||||
Future<model.Email?> getEmail(String emailId) async {
|
||||
final row = await (_db.select(_db.emails)
|
||||
..where((t) => t.id.equals(emailId)))
|
||||
.getSingleOrNull();
|
||||
@@ -42,7 +44,7 @@ class EmailRepositoryImpl implements EmailRepository {
|
||||
// ── Body (on-demand) ───────────────────────────────────────────────────────
|
||||
|
||||
@override
|
||||
Future<EmailBody> getEmailBody(String emailId) async {
|
||||
Future<model.EmailBody> getEmailBody(String emailId) async {
|
||||
final cached = await (_db.select(_db.emailBodies)
|
||||
..where((t) => t.emailId.equals(emailId)))
|
||||
.getSingleOrNull();
|
||||
@@ -56,17 +58,11 @@ class EmailRepositoryImpl implements EmailRepository {
|
||||
final client = await connectImap(account, password);
|
||||
try {
|
||||
await client.selectMailboxByPath(emailRow.mailboxPath);
|
||||
final fetch = await client.fetchMessage(
|
||||
imap.MessageSequence.fromId(emailRow.uid, isUid: true),
|
||||
'(BODY[])',
|
||||
isUidSequence: true,
|
||||
);
|
||||
final fetch = await client.uidFetchMessage(emailRow.uid, '(BODY[])');
|
||||
final msg = fetch.messages.first;
|
||||
final textBody = msg.decodeTextPlainPart();
|
||||
final htmlBody = msg.decodeTextHtmlPart();
|
||||
final contentInfos = msg.findContentInfo(
|
||||
disposition: imap.ContentDisposition.attachment,
|
||||
);
|
||||
final contentInfos = msg.findContentInfo();
|
||||
|
||||
final attachmentsJson = jsonEncode(
|
||||
contentInfos
|
||||
@@ -88,7 +84,7 @@ class EmailRepositoryImpl implements EmailRepository {
|
||||
attachmentsJson: Value(attachmentsJson),
|
||||
),
|
||||
);
|
||||
return EmailBody(
|
||||
return model.EmailBody(
|
||||
emailId: emailId,
|
||||
textBody: textBody,
|
||||
htmlBody: htmlBody,
|
||||
@@ -117,17 +113,17 @@ class EmailRepositoryImpl implements EmailRepository {
|
||||
if (envelope == null) continue;
|
||||
final uid = msg.uid;
|
||||
if (uid == null) continue;
|
||||
final emailId = '${accountId}:$uid';
|
||||
final emailId = '$accountId:$uid';
|
||||
|
||||
await _db.into(_db.emails).insertOnConflictUpdate(
|
||||
EmailsCompanion.insert(
|
||||
id: emailId,
|
||||
accountId: accountId,
|
||||
mailboxPath: mailboxPath,
|
||||
uid: Value(uid),
|
||||
uid: uid,
|
||||
subject: Value(envelope.subject),
|
||||
sentAt: Value(envelope.date),
|
||||
receivedAt: Value(envelope.date ?? DateTime.now()),
|
||||
receivedAt: envelope.date ?? DateTime.now(),
|
||||
fromJson: Value(_encodeAddresses(envelope.from)),
|
||||
toJson: Value(_encodeAddresses(envelope.to)),
|
||||
ccJson: Value(_encodeAddresses(envelope.cc)),
|
||||
@@ -145,7 +141,11 @@ class EmailRepositoryImpl implements EmailRepository {
|
||||
// ── Mutations ──────────────────────────────────────────────────────────────
|
||||
|
||||
@override
|
||||
Future<void> setFlag(String emailId, {bool? seen, bool? flagged}) async {
|
||||
Future<void> setFlag(
|
||||
String emailId, {
|
||||
bool? seen,
|
||||
bool? flagged,
|
||||
}) async {
|
||||
final row = await (_db.select(_db.emails)
|
||||
..where((t) => t.id.equals(emailId)))
|
||||
.getSingle();
|
||||
@@ -157,13 +157,13 @@ class EmailRepositoryImpl implements EmailRepository {
|
||||
final seq = imap.MessageSequence.fromId(row.uid, isUid: true);
|
||||
if (seen != null) {
|
||||
seen
|
||||
? await client.markSeen(seq, isUidSequence: true)
|
||||
: await client.markUnseen(seq, isUidSequence: true);
|
||||
? await client.uidMarkSeen(seq)
|
||||
: await client.uidMarkUnseen(seq);
|
||||
}
|
||||
if (flagged != null) {
|
||||
flagged
|
||||
? await client.markFlagged(seq, isUidSequence: true)
|
||||
: await client.markUnflagged(seq, isUidSequence: true);
|
||||
? await client.uidMarkFlagged(seq)
|
||||
: await client.uidMarkUnflagged(seq);
|
||||
}
|
||||
await (_db.update(_db.emails)..where((t) => t.id.equals(emailId)))
|
||||
.write(
|
||||
@@ -187,10 +187,9 @@ class EmailRepositoryImpl implements EmailRepository {
|
||||
final client = await connectImap(account, password);
|
||||
try {
|
||||
await client.selectMailboxByPath(row.mailboxPath);
|
||||
await client.move(
|
||||
await client.uidMove(
|
||||
imap.MessageSequence.fromId(row.uid, isUid: true),
|
||||
targetMailboxPath: destMailboxPath,
|
||||
isUidSequence: true,
|
||||
);
|
||||
await (_db.delete(_db.emails)..where((t) => t.id.equals(emailId))).go();
|
||||
} finally {
|
||||
@@ -208,10 +207,9 @@ class EmailRepositoryImpl implements EmailRepository {
|
||||
final client = await connectImap(account, password);
|
||||
try {
|
||||
await client.selectMailboxByPath(row.mailboxPath);
|
||||
await client.deleteMessages(
|
||||
imap.MessageSequence.fromId(row.uid, isUid: true),
|
||||
isUidSequence: true,
|
||||
);
|
||||
final seq = imap.MessageSequence.fromId(row.uid, isUid: true);
|
||||
await client.uidMarkDeleted(seq);
|
||||
await client.uidExpunge(seq);
|
||||
await (_db.delete(_db.emails)..where((t) => t.id.equals(emailId))).go();
|
||||
} finally {
|
||||
await client.logout();
|
||||
@@ -219,7 +217,7 @@ class EmailRepositoryImpl implements EmailRepository {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> sendEmail(String accountId, EmailDraft draft) async {
|
||||
Future<void> sendEmail(String accountId, model.EmailDraft draft) async {
|
||||
final account = (await _accounts.getAccount(accountId))!;
|
||||
final password = await _accounts.getPassword(accountId);
|
||||
final smtpClient = await connectSmtp(account, password);
|
||||
@@ -242,27 +240,26 @@ class EmailRepositoryImpl implements EmailRepository {
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
String _encodeAddresses(List<imap.MailAddress>? addresses) =>
|
||||
jsonEncode(
|
||||
String _encodeAddresses(List<imap.MailAddress>? addresses) => jsonEncode(
|
||||
(addresses ?? const [])
|
||||
.map((a) => {'name': a.personalName, 'email': a.email})
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Email _toModel(db.Email row) {
|
||||
List<EmailAddress> parseAddresses(String json) {
|
||||
final list = jsonDecode(json) as List;
|
||||
model.Email _toModel(Email row) {
|
||||
List<model.EmailAddress> parseAddresses(String json) {
|
||||
final list = jsonDecode(json) as List<dynamic>;
|
||||
return list
|
||||
.map(
|
||||
(e) => EmailAddress(
|
||||
name: e['name'] as String?,
|
||||
(e) => model.EmailAddress(
|
||||
name: (e as Map<String, dynamic>)['name'] as String?,
|
||||
email: e['email'] as String,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
return Email(
|
||||
return model.Email(
|
||||
id: row.id,
|
||||
accountId: row.accountId,
|
||||
mailboxPath: row.mailboxPath,
|
||||
@@ -280,19 +277,19 @@ class EmailRepositoryImpl implements EmailRepository {
|
||||
);
|
||||
}
|
||||
|
||||
EmailBody _bodyRowToModel(db.EmailBody row) => EmailBody(
|
||||
model.EmailBody _bodyRowToModel(EmailBody row) => model.EmailBody(
|
||||
emailId: row.emailId,
|
||||
textBody: row.textBody,
|
||||
htmlBody: row.htmlBody,
|
||||
attachments: _parseAttachments(row.attachmentsJson),
|
||||
);
|
||||
|
||||
List<EmailAttachment> _parseAttachments(String json) {
|
||||
final list = jsonDecode(json) as List;
|
||||
List<model.EmailAttachment> _parseAttachments(String json) {
|
||||
final list = jsonDecode(json) as List<dynamic>;
|
||||
return list
|
||||
.map(
|
||||
(e) => EmailAttachment(
|
||||
filename: e['filename'] as String,
|
||||
(e) => model.EmailAttachment(
|
||||
filename: (e as Map<String, dynamic>)['filename'] as String,
|
||||
contentType: e['contentType'] as String,
|
||||
size: e['size'] as int,
|
||||
),
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:enough_mail/enough_mail.dart' as imap;
|
||||
|
||||
import '../../core/models/mailbox.dart';
|
||||
import '../../core/models/mailbox.dart' as model;
|
||||
import '../../core/repositories/account_repository.dart';
|
||||
import '../../core/repositories/mailbox_repository.dart';
|
||||
import '../db/database.dart';
|
||||
import '../db/database.dart' as db show Mailbox;
|
||||
import '../../core/utils/logger.dart';
|
||||
import '../db/database.dart';
|
||||
import '../imap/imap_client_factory.dart';
|
||||
|
||||
class MailboxRepositoryImpl implements MailboxRepository {
|
||||
@@ -16,7 +15,7 @@ class MailboxRepositoryImpl implements MailboxRepository {
|
||||
final AccountRepository _accounts;
|
||||
|
||||
@override
|
||||
Stream<List<Mailbox>> observeMailboxes(String accountId) {
|
||||
Stream<List<model.Mailbox>> observeMailboxes(String accountId) {
|
||||
return (_db.select(_db.mailboxes)
|
||||
..where((t) => t.accountId.equals(accountId))
|
||||
..orderBy([(t) => OrderingTerm.asc(t.path)]))
|
||||
@@ -30,16 +29,15 @@ class MailboxRepositoryImpl implements MailboxRepository {
|
||||
final password = await _accounts.getPassword(accountId);
|
||||
final client = await connectImap(account, password);
|
||||
try {
|
||||
// listMailboxes() returns List<imap.Mailbox>
|
||||
final mailboxes = await client.listMailboxes(recursive: true);
|
||||
for (final mb in mailboxes) {
|
||||
final path = mb.path;
|
||||
final id = '${accountId}:$path';
|
||||
final id = '$accountId:$path';
|
||||
|
||||
// Fetch STATUS (unread + total counts) for each mailbox.
|
||||
// Suppress errors — some mailboxes (e.g. \Noselect) can't be selected.
|
||||
int unread = 0;
|
||||
int total = 0;
|
||||
// Fetch STATUS (unread + total counts). Some mailboxes (\Noselect)
|
||||
// can't be selected — skip counts for those silently.
|
||||
var unread = 0;
|
||||
var total = 0;
|
||||
try {
|
||||
final status = await client.statusMailbox(
|
||||
mb,
|
||||
@@ -48,7 +46,6 @@ class MailboxRepositoryImpl implements MailboxRepository {
|
||||
unread = status.messagesUnseen;
|
||||
total = status.messagesExists;
|
||||
} catch (e) {
|
||||
// \Noselect mailboxes can't be STATUSed — skip counts silently.
|
||||
log('STATUS skipped for $path: $e');
|
||||
}
|
||||
|
||||
@@ -68,7 +65,7 @@ class MailboxRepositoryImpl implements MailboxRepository {
|
||||
}
|
||||
}
|
||||
|
||||
Mailbox _toModel(db.Mailbox row) => Mailbox(
|
||||
model.Mailbox _toModel(MailboxRow row) => model.Mailbox(
|
||||
id: row.id,
|
||||
accountId: row.accountId,
|
||||
path: row.path,
|
||||
|
||||
@@ -61,6 +61,7 @@ class _AddAccountScreenState extends ConsumerState<AddAccountScreen> {
|
||||
.addAccount(account, _password.text);
|
||||
if (mounted) context.pop();
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(SnackBar(content: Text('Error: $e')));
|
||||
} finally {
|
||||
|
||||
@@ -83,6 +83,7 @@ class _ComposeScreenState extends ConsumerState<ComposeScreen> {
|
||||
await ref.read(emailRepositoryProvider).sendEmail(_accountId!, draft);
|
||||
if (mounted) context.pop();
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(SnackBar(content: Text('Send failed: $e')));
|
||||
} finally {
|
||||
|
||||
@@ -16,28 +16,26 @@ String _env(String key) {
|
||||
return v;
|
||||
}
|
||||
|
||||
ImapClient _makeClient() => ImapClient(isLogEnabled: false);
|
||||
|
||||
Future<ImapClient> _connect(
|
||||
String user,
|
||||
String pass, {
|
||||
String host = '127.0.0.1',
|
||||
int? port,
|
||||
required String host,
|
||||
required int port,
|
||||
}) async {
|
||||
final p = port ?? int.parse(_env('STALWART_IMAP_PORT'));
|
||||
final client = _makeClient();
|
||||
await client.connectToServer(host, p, isSecure: false);
|
||||
final client = ImapClient();
|
||||
await client.connectToServer(host, port, isSecure: false);
|
||||
await client.login(user, pass);
|
||||
return client;
|
||||
}
|
||||
|
||||
void main() {
|
||||
final imapHost = Platform.environment['STALWART_IMAP_HOST'] ?? '127.0.0.1';
|
||||
late String imapHost;
|
||||
late int imapPort;
|
||||
late int smtpPort;
|
||||
late String userA, passA, userB, passB;
|
||||
|
||||
setUpAll(() {
|
||||
imapHost = Platform.environment['STALWART_IMAP_HOST'] ?? '127.0.0.1';
|
||||
imapPort = int.parse(_env('STALWART_IMAP_PORT'));
|
||||
smtpPort = int.parse(_env('STALWART_SMTP_PORT'));
|
||||
userA = _env('STALWART_USER_B'); // alice
|
||||
@@ -47,26 +45,25 @@ void main() {
|
||||
});
|
||||
|
||||
test('login and list mailboxes', () async {
|
||||
final client = await _connect(userA, passA, host: imapHost, port: imapPort);
|
||||
final client = await _connect(
|
||||
userA,
|
||||
passA,
|
||||
host: imapHost,
|
||||
port: imapPort,
|
||||
);
|
||||
addTearDown(() => client.logout().ignore());
|
||||
|
||||
final response = await client.listMailboxes();
|
||||
expect(response.mailboxes, isNotEmpty);
|
||||
expect(
|
||||
response.mailboxes!.map((m) => m.name),
|
||||
contains('INBOX'),
|
||||
);
|
||||
// listMailboxes() returns List<Mailbox> directly
|
||||
final mailboxes = await client.listMailboxes();
|
||||
expect(mailboxes, isNotEmpty);
|
||||
expect(mailboxes.map((m) => m.name), contains('INBOX'));
|
||||
});
|
||||
|
||||
test('send via SMTP and receive via IMAP', () async {
|
||||
final smtpClient = SmtpClient('test', isLogEnabled: false);
|
||||
final smtpClient = SmtpClient('test');
|
||||
await smtpClient.connectToServer(imapHost, smtpPort, isSecure: false);
|
||||
await smtpClient.ehlo();
|
||||
await smtpClient.authenticate(
|
||||
'$userA@localhost',
|
||||
passA,
|
||||
AuthMechanism.plain,
|
||||
);
|
||||
await smtpClient.authenticate('$userA@localhost', passA);
|
||||
|
||||
final builder = MessageBuilder()
|
||||
..from = [MailAddress('Alice', '$userA@localhost')]
|
||||
@@ -74,16 +71,20 @@ void main() {
|
||||
..subject = 'Integration test ${DateTime.now().millisecondsSinceEpoch}'
|
||||
..text = 'Hello from SharedInbox integration test.';
|
||||
await smtpClient.sendMessage(builder.buildMimeMessage());
|
||||
smtpClient.disconnect();
|
||||
await smtpClient.quit();
|
||||
|
||||
// Give Stalwart a moment to deliver the message.
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
await Future<void>.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
final imapClient =
|
||||
await _connect(userB, passB, host: imapHost, port: imapPort);
|
||||
final imapClient = await _connect(
|
||||
userB,
|
||||
passB,
|
||||
host: imapHost,
|
||||
port: imapPort,
|
||||
);
|
||||
addTearDown(() => imapClient.logout().ignore());
|
||||
|
||||
final mailbox = await imapClient.selectMailboxByPath('INBOX');
|
||||
expect(mailbox.messagesExists, greaterThan(0));
|
||||
final inbox = await imapClient.selectMailboxByPath('INBOX');
|
||||
expect(inbox.messagesExists, greaterThan(0));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -13,11 +13,11 @@ String encodeAddresses(List<EmailAddress> addresses) => jsonEncode(
|
||||
);
|
||||
|
||||
List<EmailAddress> decodeAddresses(String json) {
|
||||
final list = jsonDecode(json) as List;
|
||||
final list = jsonDecode(json) as List<dynamic>;
|
||||
return list
|
||||
.map(
|
||||
(e) => EmailAddress(
|
||||
name: e['name'] as String?,
|
||||
name: (e as Map<String, dynamic>)['name'] as String?,
|
||||
email: e['email'] as String,
|
||||
),
|
||||
)
|
||||
@@ -78,9 +78,9 @@ void main() {
|
||||
|
||||
group('EmailDraft', () {
|
||||
test('constructs with required fields', () {
|
||||
final draft = EmailDraft(
|
||||
from: const EmailAddress(name: 'Me', email: 'me@example.com'),
|
||||
to: [const EmailAddress(email: 'you@example.com')],
|
||||
const draft = EmailDraft(
|
||||
from: EmailAddress(name: 'Me', email: 'me@example.com'),
|
||||
to: [EmailAddress(email: 'you@example.com')],
|
||||
cc: [],
|
||||
subject: 'Hello',
|
||||
body: 'World',
|
||||
|
||||
@@ -21,7 +21,7 @@ void main() {
|
||||
});
|
||||
|
||||
test('converts <p> to newline', () {
|
||||
expect(htmlToPlain('<p>paragraph</p>'), '\nparagraph');
|
||||
expect(htmlToPlain('<p>paragraph</p>'), 'paragraph');
|
||||
});
|
||||
|
||||
test('decodes &', () {
|
||||
@@ -53,7 +53,7 @@ void main() {
|
||||
});
|
||||
|
||||
test('handles nested tags', () {
|
||||
expect(htmlToPlain('<div><p>text</p></div>'), '\ntext');
|
||||
expect(htmlToPlain('<div><p>text</p></div>'), 'text');
|
||||
});
|
||||
|
||||
test('real-world HTML email snippet', () {
|
||||
|
||||
Reference in New Issue
Block a user