From 3fad83a87adc0dafa611b33d590acbfaac848f44 Mon Sep 17 00:00:00 2001 From: Robert Virkus Date: Mon, 5 Jun 2023 08:02:40 +0200 Subject: [PATCH 01/26] improve code style, fix test --- analysis_options.yaml | 1 - lib/src/discover/discover.dart | 2 +- lib/src/imap/message_sequence.dart | 2 ++ lib/src/message_builder.dart | 2 ++ lib/src/mime_message.dart | 2 ++ lib/src/private/imap/capability_parser.dart | 2 +- lib/src/private/imap/enable_parser.dart | 2 +- lib/src/private/util/discover_helper.dart | 8 ++++---- lib/src/private/util/uint8_list_reader.dart | 7 +++++-- pubspec.yaml | 2 +- test/mail/mail_account_test.dart | 1 + test/message_builder_test.dart | 5 ++++- 12 files changed, 24 insertions(+), 12 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 8642358f..94071928 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -93,7 +93,6 @@ linter: - prefer_const_literals_to_create_immutables - prefer_constructors_over_static_methods - prefer_contains - - prefer_equal_for_default_values - prefer_expression_function_bodies - prefer_final_fields - prefer_final_in_for_each diff --git a/lib/src/discover/discover.dart b/lib/src/discover/discover.dart index 37720183..4b7c846b 100644 --- a/lib/src/discover/discover.dart +++ b/lib/src/discover/discover.dart @@ -126,7 +126,7 @@ class Discover { } // try to guess incoming and outgoing server names based on the domain - final domains = hasMxDomain ? [emailDomain, mxDomain!] : [emailDomain]; + final domains = hasMxDomain ? [emailDomain, mxDomain] : [emailDomain]; config ??= await DiscoverHelper.discoverFromCommonDomains(domains, isLogEnabled: isLogEnabled); } diff --git a/lib/src/imap/message_sequence.dart b/lib/src/imap/message_sequence.dart index 893a2d39..fb839c41 100644 --- a/lib/src/imap/message_sequence.dart +++ b/lib/src/imap/message_sequence.dart @@ -1,3 +1,5 @@ +// ignore_for_file: avoid_returning_this + import 'dart:collection'; import 'package:json_annotation/json_annotation.dart'; diff --git a/lib/src/message_builder.dart b/lib/src/message_builder.dart index eb76d6c8..3e39f8bc 100644 --- a/lib/src/message_builder.dart +++ b/lib/src/message_builder.dart @@ -1,3 +1,5 @@ +// ignore_for_file: avoid_returning_this + import 'dart:convert'; import 'dart:io'; import 'dart:math' as math; diff --git a/lib/src/mime_message.dart b/lib/src/mime_message.dart index 6309ca80..38b68343 100644 --- a/lib/src/mime_message.dart +++ b/lib/src/mime_message.dart @@ -1,3 +1,5 @@ +// ignore_for_file: avoid_returning_this + import 'dart:typed_data'; import 'package:collection/collection.dart' show IterableExtension; diff --git a/lib/src/private/imap/capability_parser.dart b/lib/src/private/imap/capability_parser.dart index a1b33b8f..e38596ea 100644 --- a/lib/src/private/imap/capability_parser.dart +++ b/lib/src/private/imap/capability_parser.dart @@ -55,7 +55,7 @@ class CapabilityParser extends ResponseParser> { } info.capabilitiesText = capText; final capNames = capText.split(' '); - final caps = capNames.map((name) => Capability(name)).toList(); + final caps = capNames.map(Capability.new).toList(); info.capabilities = caps; } } diff --git a/lib/src/private/imap/enable_parser.dart b/lib/src/private/imap/enable_parser.dart index b712be7d..b8f5351e 100644 --- a/lib/src/private/imap/enable_parser.dart +++ b/lib/src/private/imap/enable_parser.dart @@ -35,7 +35,7 @@ class EnableParser extends ResponseParser> { void parseCapabilities(String details, int startIndex) { final capText = details.substring(startIndex); final capNames = capText.split(' '); - final caps = capNames.map((name) => Capability(name)); + final caps = capNames.map(Capability.new); info.enabledCapabilities.addAll(caps); } } diff --git a/lib/src/private/util/discover_helper.dart b/lib/src/private/util/discover_helper.dart index e27a474f..11ea8208 100644 --- a/lib/src/private/util/discover_helper.dart +++ b/lib/src/private/util/discover_helper.dart @@ -292,13 +292,13 @@ class DiscoverHelper { if (providerChild is xml.XmlElement) { switch (providerChild.name.local) { case 'domain': - provider.addDomain(providerChild.text); + provider.addDomain(providerChild.innerText); break; case 'displayName': - provider.displayName = providerChild.text; + provider.displayName = providerChild.innerText; break; case 'displayShortName': - provider.displayShortName = providerChild.text; + provider.displayShortName = providerChild.innerText; break; case 'incomingServer': provider @@ -336,7 +336,7 @@ class DiscoverHelper { ..typeName = serverElement.getAttribute('type') ?? 'unknown'; for (final childNode in serverElement.children) { if (childNode is xml.XmlElement) { - final text = childNode.text; + final text = childNode.innerText; switch (childNode.name.local) { case 'hostname': server.hostname = text; diff --git a/lib/src/private/util/uint8_list_reader.dart b/lib/src/private/util/uint8_list_reader.dart index c6cf8a81..05954f43 100644 --- a/lib/src/private/util/uint8_list_reader.dart +++ b/lib/src/private/util/uint8_list_reader.dart @@ -203,8 +203,11 @@ class OptimizedBytesBuilder { } i -= chunk.length; } - throw IndexError(index, this, 'unknown', - 'for index $index in builder with length $length', _length); + throw IndexError.withLength( + index, + length, + message: 'for index $index in builder with length $length', + ); } /// Tries to the find the position of the first CR-LF line break diff --git a/pubspec.yaml b/pubspec.yaml index 95ce4db6..d6fd48f4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ version: 2.1.1 homepage: https://github.com/Enough-Software/enough_mail environment: - sdk: '>=2.12.0 <3.0.0' + sdk: '>=2.19.0 <4.0.0' dependencies: basic_utils: ^5.4.2 diff --git a/test/mail/mail_account_test.dart b/test/mail/mail_account_test.dart index aa507972..37e75474 100644 --- a/test/mail/mail_account_test.dart +++ b/test/mail/mail_account_test.dart @@ -201,6 +201,7 @@ void main() { final jsonText = jsonEncode(jsonAccountsList); final jsonList = jsonDecode(jsonText) as List; final parsedAccounts = + // ignore: unnecessary_lambdas jsonList.map((json) => MailAccount.fromJson(json)).toList(); expect(parsedAccounts.length, accounts.length); for (var i = 0; i < accounts.length; i++) { diff --git a/test/message_builder_test.dart b/test/message_builder_test.dart index 4b70e3c8..62ad120f 100644 --- a/test/message_builder_test.dart +++ b/test/message_builder_test.dart @@ -582,7 +582,10 @@ END:VCARD\r '"=?utf8?Q?One_m=C3=B6re?=" '); expect(message.getHeaderValue('Content-Type'), 'text/plain; charset="utf-8"'); - expect(message.getHeaderValue('Content-Transfer-Encoding'), '7bit'); + expect( + message.getHeaderValue('Content-Transfer-Encoding'), + 'quoted-printable', + ); const expectedStart = 'Here is my reply\r\n>On '; expect(message.decodeContentText()?.substring(0, expectedStart.length), expectedStart); From 41ac9747836dc504e9261367207aab0af3601ca8 Mon Sep 17 00:00:00 2001 From: Robert Virkus Date: Mon, 5 Jun 2023 08:05:08 +0200 Subject: [PATCH 02/26] format code --- lib/src/codecs/date_codec.dart | 1 - lib/src/codecs/mail_codec.dart | 10 ++++++---- lib/src/mail_conventions.dart | 1 - 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/src/codecs/date_codec.dart b/lib/src/codecs/date_codec.dart index e781928d..c571de79 100644 --- a/lib/src/codecs/date_codec.dart +++ b/lib/src/codecs/date_codec.dart @@ -542,5 +542,4 @@ Date and time values occur in several header fields. This section return dateTime.toLocal(); } // cSpell:enable - } diff --git a/lib/src/codecs/mail_codec.dart b/lib/src/codecs/mail_codec.dart index 2123b043..6f440d5d 100644 --- a/lib/src/codecs/mail_codec.dart +++ b/lib/src/codecs/mail_codec.dart @@ -102,10 +102,12 @@ abstract class MailCodec { 'us-ascii': () => encodingAscii, 'ascii': () => encodingAscii, }; - static final _textDecodersByName = < - String, - String Function(String text, convert.Encoding encoding, - {required bool isHeader})>{ + static final _textDecodersByName = { 'q': quotedPrintable.decodeText, 'quoted-printable': quotedPrintable.decodeText, 'b': base64.decodeText, diff --git a/lib/src/mail_conventions.dart b/lib/src/mail_conventions.dart index d9048740..72e0d67d 100644 --- a/lib/src/mail_conventions.dart +++ b/lib/src/mail_conventions.dart @@ -158,5 +158,4 @@ This is no guarantee that the message has been read or understood.'''; /// The `Return-Path` header static const String headerReturnPath = 'Return-Path'; //static const String header = ''; - } From e18b7492251cd41b3e73f417f24c52b8390259db Mon Sep 17 00:00:00 2001 From: Robert Virkus Date: Mon, 5 Jun 2023 08:05:34 +0200 Subject: [PATCH 03/26] Create CI --- .github/workflows/dart.yml | 45 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 .github/workflows/dart.yml diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml new file mode 100644 index 00000000..47cc43b6 --- /dev/null +++ b/.github/workflows/dart.yml @@ -0,0 +1,45 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Dart + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + # Note: This workflow uses the latest stable version of the Dart SDK. + # You can specify other versions if desired, see documentation here: + # https://github.com/dart-lang/setup-dart/blob/main/README.md + # - uses: dart-lang/setup-dart@v1 + - uses: dart-lang/setup-dart@9a04e6d73cca37bd455e0608d7e5092f881fd603 + + - name: Output dart version + run: dart --version + + - name: Install dependencies + run: dart pub get + + # Uncomment this step to verify the use of 'dart format' on each commit. + - name: Verify formatting + run: dart format --output=none --set-exit-if-changed . + + # Consider passing '--fatal-infos' for slightly stricter analysis. + - name: Analyze project source + run: dart analyze + + # Your project will need to have tests in test/ and a dependency on + # package:test for this step to succeed. Note that Flutter projects will + # want to change this to 'flutter test'. + - name: Run tests + run: dart test From 7acfdd1a57c40f51254c1c3a71d6f11d707504fb Mon Sep 17 00:00:00 2001 From: eymeric Date: Wed, 26 Jul 2023 18:49:11 +0200 Subject: [PATCH 04/26] update deps --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index d6fd48f4..80cd41ec 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,7 +7,7 @@ environment: sdk: '>=2.19.0 <4.0.0' dependencies: - basic_utils: ^5.4.2 + basic_utils: ^5.6.1 collection: ^1.16.0 crypto: ^3.0.0 encrypt: ^5.0.0 From fc3084d2a1274497b633bcbd7d547624131a5222 Mon Sep 17 00:00:00 2001 From: Robert Virkus Date: Sun, 24 Sep 2023 15:03:01 +0200 Subject: [PATCH 05/26] chore: improve documentation --- README.md | 2 +- lib/src/mail/mail_account.dart | 26 +++------------------ lib/src/private/util/uint8_list_reader.dart | 1 + 3 files changed, 5 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 4bf39c37..6faf52f7 100644 --- a/README.md +++ b/README.md @@ -334,7 +334,7 @@ Transfer encodings: * Test cases are in *test*. * Please file a pull request for each improvement/fix that you are create - your contributions are welcome. * Check out https://github.com/enough-Software/enough_mail/contribute for good first issues. -* When changing model files, re-run the code generation by calling `flutter pub run build_runner build --delete-conflicting-outputs`. +* When changing model files, re-run the code generation by calling `dart run build_runner build --delete-conflicting-outputs`. ## License diff --git a/lib/src/mail/mail_account.dart b/lib/src/mail/mail_account.dart index dfca10bc..24316e38 100644 --- a/lib/src/mail/mail_account.dart +++ b/lib/src/mail/mail_account.dart @@ -259,7 +259,7 @@ class MailAccount { /// by default an unmodifiable `const {}` is used. final Map attributes; - /// Checks if this account has an attribute with the specified name + /// Checks if this account has an attribute with the specified [name] bool hasAttribute(String name) => attributes.containsKey(name); /// Retrieves the user name from the given [email] and @@ -323,17 +323,7 @@ class MailAccount { this.attributes.isEmpty ? {} : this.attributes; attributes[name] = value; - return MailAccount( - name: name, - email: email, - userName: userName, - incoming: incoming, - outgoing: outgoing, - aliases: aliases, - outgoingClientDomain: outgoingClientDomain, - supportsPlusAliases: supportsPlusAliases, - attributes: attributes, - ); + return copyWith(attributes: attributes); } /// Copies this account with the additional [alias] @@ -343,17 +333,7 @@ class MailAccount { final aliases = this.aliases.isEmpty ? [] : this.aliases ..add(alias); - return MailAccount( - name: name, - email: email, - userName: userName, - incoming: incoming, - outgoing: outgoing, - aliases: aliases, - outgoingClientDomain: outgoingClientDomain, - supportsPlusAliases: supportsPlusAliases, - attributes: attributes, - ); + return copyWith(aliases: aliases); } /// Convenience method to update the incoming and outgoing authentication diff --git a/lib/src/private/util/uint8_list_reader.dart b/lib/src/private/util/uint8_list_reader.dart index 05954f43..35b9e4c0 100644 --- a/lib/src/private/util/uint8_list_reader.dart +++ b/lib/src/private/util/uint8_list_reader.dart @@ -206,6 +206,7 @@ class OptimizedBytesBuilder { throw IndexError.withLength( index, length, + name: 'index', message: 'for index $index in builder with length $length', ); } From 7616a2ec24767b91af876904a86a13d6a1793fa2 Mon Sep 17 00:00:00 2001 From: Robert Virkus Date: Sun, 24 Sep 2023 16:03:10 +0200 Subject: [PATCH 06/26] feat: support more name variations for ISO codecs --- lib/src/codecs/mail_codec.dart | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/src/codecs/mail_codec.dart b/lib/src/codecs/mail_codec.dart index 6f440d5d..51cc69bb 100644 --- a/lib/src/codecs/mail_codec.dart +++ b/lib/src/codecs/mail_codec.dart @@ -50,21 +50,36 @@ abstract class MailCodec { 'utf8': () => encodingUtf8, 'latin-1': () => encodingLatin1, 'iso-8859-1': () => encodingLatin1, + 'iso8859-1': () => encodingLatin1, 'iso-8859-2': () => const Latin2Codec(allowInvalid: true), + 'iso8859-2': () => const Latin2Codec(allowInvalid: true), 'iso-8859-3': () => const Latin3Codec(allowInvalid: true), + 'iso8859-3': () => const Latin3Codec(allowInvalid: true), 'iso-8859-4': () => const Latin4Codec(allowInvalid: true), + 'iso8859-4': () => const Latin4Codec(allowInvalid: true), 'iso-8859-5': () => const Latin5Codec(allowInvalid: true), + 'iso8859-5': () => const Latin5Codec(allowInvalid: true), 'iso-8859-6': () => const Latin6Codec(allowInvalid: true), + 'iso8859-6': () => const Latin6Codec(allowInvalid: true), 'iso-8859-7': () => const Latin7Codec(allowInvalid: true), + 'iso8859-7': () => const Latin7Codec(allowInvalid: true), 'iso-8859-8': () => const Latin8Codec(allowInvalid: true), + 'iso8859-8': () => const Latin8Codec(allowInvalid: true), 'iso-8859-9': () => const Latin9Codec(allowInvalid: true), + 'iso8859-9': () => const Latin9Codec(allowInvalid: true), 'iso-8859-10': () => const Latin10Codec(allowInvalid: true), + 'iso8859-10': () => const Latin10Codec(allowInvalid: true), 'iso-8859-11': () => const Latin11Codec(allowInvalid: true), + 'iso8859-11': () => const Latin11Codec(allowInvalid: true), // iso-8859-12 does not exist... 'iso-8859-13': () => const Latin13Codec(allowInvalid: true), + 'iso8859-13': () => const Latin13Codec(allowInvalid: true), 'iso-8859-14': () => const Latin14Codec(allowInvalid: true), + 'iso8859-14': () => const Latin14Codec(allowInvalid: true), 'iso-8859-15': () => const Latin15Codec(allowInvalid: true), + 'iso8859-15': () => const Latin15Codec(allowInvalid: true), 'iso-8859-16': () => const Latin16Codec(allowInvalid: true), + 'iso8859-16': () => const Latin16Codec(allowInvalid: true), 'windows-1250': () => const Windows1250Codec(allowInvalid: true), 'cp1250': () => const Windows1250Codec(allowInvalid: true), 'cp-1250': () => const Windows1250Codec(allowInvalid: true), From af6ae125b13648d3cb8670634c4e90eccdd83727 Mon Sep 17 00:00:00 2001 From: Olaf Groeger Date: Thu, 5 Oct 2023 10:49:26 +0200 Subject: [PATCH 07/26] Fix RangeError when a Mailbox name contains a "(" - Added implementation in status_parser.dart - Added test in status_parser_test.dart --- lib/src/private/imap/status_parser.dart | 14 ++++++++++++-- test/src/imap/status_parser_test.dart | 17 +++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/lib/src/private/imap/status_parser.dart b/lib/src/private/imap/status_parser.dart index e9e6623a..9d3aa4f5 100644 --- a/lib/src/private/imap/status_parser.dart +++ b/lib/src/private/imap/status_parser.dart @@ -6,11 +6,13 @@ import 'response_parser.dart'; /// Parses status responses class StatusParser extends ResponseParser { /// Creates a new parser - StatusParser(this.box); + StatusParser(this.box) : _regex = RegExp(r'(STATUS "[^"]+?" )(.*)'); /// The current mailbox Mailbox box; + final RegExp _regex; + @override Mailbox? parse(ImapResponse imapResponse, Response response) => response.isOkStatus ? box : null; @@ -19,7 +21,7 @@ class StatusParser extends ResponseParser { bool parseUntagged(ImapResponse imapResponse, Response? response) { final details = imapResponse.parseText; if (details.startsWith('STATUS ')) { - final startIndex = details.indexOf('('); + final startIndex = _findStartIndex(details); if (startIndex == -1) { return false; } @@ -56,4 +58,12 @@ class StatusParser extends ResponseParser { return super.parseUntagged(imapResponse, response); } } + + int _findStartIndex(String details) { + final matches = _regex.allMatches(details); + if (matches.isNotEmpty && matches.first.groupCount == 2) { + return matches.first.group(1)!.length; + } + return -1; + } } diff --git a/test/src/imap/status_parser_test.dart b/test/src/imap/status_parser_test.dart index 8e811262..6244118e 100644 --- a/test/src/imap/status_parser_test.dart +++ b/test/src/imap/status_parser_test.dart @@ -41,4 +41,21 @@ void main() { expect(box.uidValidity, 2222); expect(box.uidNext, 876); }); + + test('Status of Mailbox with name containing brackets', () { + const responseText = + 'STATUS "upper level.Funny folder (with brackets)" (MESSAGES 2)'; + final details = ImapResponse()..add(ImapResponseLine(responseText)); + final box = Mailbox( + encodedName: 'Funny folder (with brackets)', + encodedPath: 'upper level.Funny folder (with brackets)', + flags: [MailboxFlag.junk], + pathSeparator: '.', + ); + final parser = StatusParser(box); + final response = Response()..status = ResponseStatus.ok; + final processed = parser.parseUntagged(details, response); + expect(processed, true); + expect(box.messagesExists, 2); + }); } From e656b6c46f78df7f47368ca7d1fdeac1bdae2996 Mon Sep 17 00:00:00 2001 From: Robert Virkus Date: Thu, 5 Oct 2023 16:44:20 +0200 Subject: [PATCH 08/26] fix: decode header problem when base64 encoding is marked with lowercase b --- analysis_options.yaml | 3 +- lib/src/codecs/mail_codec.dart | 9 +++--- lib/src/private/imap/fetch_parser.dart | 9 +++++- lib/src/private/util/mail_address_parser.dart | 10 +++++-- test/src/imap/fetch_parser_test.dart | 29 +++++++++++++++++++ 5 files changed, 50 insertions(+), 10 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 94071928..f57ba05b 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -50,6 +50,7 @@ linter: - cascade_invocations - cast_nullable_to_non_nullable - close_sinks + - collection_methods_unrelated_type - comment_references - constant_identifier_names - control_flow_in_finally @@ -63,12 +64,10 @@ linter: #- flutter_style_todos # Flutter todos are to verbose for our requirements. - hash_and_equals - implementation_imports - - iterable_contains_unrelated_type # - join_return_with_assignment # leads to less readable code IMHO - library_names - library_prefixes - lines_longer_than_80_chars - - list_remove_unrelated_type - literal_only_boolean_expressions - no_adjacent_strings_in_list - no_duplicate_case_values diff --git a/lib/src/codecs/mail_codec.dart b/lib/src/codecs/mail_codec.dart index 51cc69bb..8cf3762a 100644 --- a/lib/src/codecs/mail_codec.dart +++ b/lib/src/codecs/mail_codec.dart @@ -198,7 +198,7 @@ abstract class MailCodec { : containsEncodedWordsWithTab ? '?=\t$startSequence' : '?=$startSequence'; - if (startSequence.endsWith('?B?')) { + if (startSequence.endsWith('?B?') || startSequence.endsWith('?b?')) { // in base64 encoding there are 2 cases: // 1. individual parts can end with the padding character "=": // - in that case we just remove the @@ -274,10 +274,9 @@ abstract class MailCodec { return HeaderEncoding.none; } final group = match.group(0); - if (group?.contains('?B?') ?? false) { - return HeaderEncoding.B; - } - return HeaderEncoding.Q; + return group?.contains('?B?') ?? group?.contains('?b?') ?? false + ? HeaderEncoding.B + : HeaderEncoding.Q; } /// Decodes the given binary [text] diff --git a/lib/src/private/imap/fetch_parser.dart b/lib/src/private/imap/fetch_parser.dart index 05c2dd70..ccd8f9eb 100644 --- a/lib/src/private/imap/fetch_parser.dart +++ b/lib/src/private/imap/fetch_parser.dart @@ -586,8 +586,15 @@ class FetchParser extends ResponseParser { 'both mailboxName and hostName are null'); return null; } + String? personalName = ''; + try { + personalName = MailCodec.decodeHeader(_checkForNil(children[0].value)); + } catch (e) { + print('Warning: invalid mail address in $addressValue: ' + 'personalName is invalid: $e'); + } return MailAddress.fromEnvelope( - personalName: MailCodec.decodeHeader(_checkForNil(children[0].value)), + personalName: personalName, //sourceRoute: _checkForNil(children[1].value), mailboxName: mailboxName ?? '', hostName: hostName ?? '', diff --git a/lib/src/private/util/mail_address_parser.dart b/lib/src/private/util/mail_address_parser.dart index d49f1505..5ba8b9e0 100644 --- a/lib/src/private/util/mail_address_parser.dart +++ b/lib/src/private/util/mail_address_parser.dart @@ -61,7 +61,8 @@ class MailAddressParser { final emailWord = _findEmailAddress(addressPart); if (emailWord == null) { print( - 'Warning: no valid email address: [$addressPart] in [$emailText]'); + 'Warning: no valid email address: [$addressPart] in [$emailText]', + ); continue; } var name = emailWord.startIndex == 0 @@ -73,7 +74,12 @@ class MailAddressParser { } name = name.replaceAll(r'\"', '"'); if (name.contains('=?')) { - name = MailCodec.decodeHeader(name); + try { + name = MailCodec.decodeHeader(name); + } catch (e) { + print('Unable to decode personal name "$name": $e'); + name = ''; + } } } final address = MailAddress(name, emailWord.text); diff --git a/test/src/imap/fetch_parser_test.dart b/test/src/imap/fetch_parser_test.dart index b6325c31..7a8bf42a 100644 --- a/test/src/imap/fetch_parser_test.dart +++ b/test/src/imap/fetch_parser_test.dart @@ -1219,6 +1219,35 @@ void main() { 'Anzeige "Küchenutensilien, Käsemesser" erfolgreich veröffentlicht.'); }); + test('ENVELOPE 5 with base-encoded personal name in email', () { + const responseTexts = [ + '''* 69457 FETCH (UID 366113 RFC822.SIZE 67087 ENVELOPE ("Tue, 26 Sep 2023 10:37:26 -0400" "New Release: Modernize Applications Faster Than Ever" (("=?utf-8?b?VGhl4oCvVGVsZXJpayAm4oCvS2VuZG8gVUk=?= =?utf-8?b?IFRlYW1z4oCvYXQgUHJvZ3Jlc3PigK8=?=" NIL "progress" "products.progress.com")) (("=?utf-8?b?VGhl4oCvVGVsZXJpayAm4oCvS2VuZG8gVUk=?= =?utf-8?b?IFRlYW1z4oCvYXQgUHJvZ3Jlc3PigK8=?=" NIL "progress" "products.progress.com")) (("=?utf-8?b?VGhl4oCvVGVsZXJpayAm4oCvS2VuZG8gVUk=?= =?utf-8?b?IFRlYW1z4oCvYXQgUHJvZ3Jlc3PigK8=?=" NIL "replytosales" "progress.com")) ((NIL NIL "robert.virkus" "enough.de")) NIL NIL NIL "") FLAGS (\Seen))''', + ]; + final details = ImapResponse(); + for (final text in responseTexts) { + details.add(ImapResponseLine(text)); + } + final parser = FetchParser(isUidFetch: false); + final response = Response()..status = ResponseStatus.ok; + final processed = parser.parseUntagged(details, response); + expect(processed, true); + final messages = parser.parse(details, response)!.messages; + expect(messages, isNotNull); + expect(messages.length, 1); + expect(messages[0].decodeSubject(), + 'New Release: Modernize Applications Faster Than Ever'); + expect(messages[0].uid, 366113); + expect(messages[0].size, 67087); + expect(messages[0].flags, ['Seen']); + expect(messages[0].from, isNotNull); + expect(messages[0].from!.length, 1); + expect(messages[0].from![0].email, 'progress@products.progress.com'); + expect( + messages[0].from![0].personalName, + 'The Telerik & Kendo UI Teams at Progress ', + ); + }); + test('measure performance', () { const responseTexts = [ r'* 61792 FETCH (UID 347524 RFC822.SIZE 4579 ENVELOPE ("Sun, 9 Aug 2020 09:03:12 +0200 (CEST)" "Re: Your Query about \"Table\"" (("=?ISO-8859-1?Q?C=2E_Sender_=FCber_eBay_Kleinanzeigen?=" NIL "anbieter-sdkjskjfkd" "mail.ebay-kleinanzeigen.de")) (("=?ISO-8859-1?Q?C=2E_Sender_=FCber_eBay_Kleinanzeigen?=" NIL "anbieter-sdkjskjfkd" "mail.ebay-kleinanzeigen.de")) (("=?ISO-8859-1?Q?C=2E_Sender_=FCber_eBay_Kleinanzeigen?=" NIL "anbieter-sdkjskjfkd" "mail.ebay-kleinanzeigen.de")) ((NIL NIL "recipient" "enough.de")) NIL NIL NIL "<9jbzp5olgc9n54qwutoty0pnxunmoyho5ugshxplpvudvurjwh3a921kjdwkpwrf9oe06g95k69t@mail.ebay-kleinanzeigen.de>") FLAGS (\Seen))' From d83c966e48863776b2a90fdcd5bd64b3318901a8 Mon Sep 17 00:00:00 2001 From: Robert Virkus Date: Thu, 5 Oct 2023 16:54:54 +0200 Subject: [PATCH 09/26] feat: release v2.1.2 --- CHANGELOG.md | 8 ++++++++ pubspec.yaml | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f400563e..549266bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# 2.1.2 +* Fix: RangeError when a Mailbox name contains a parentheses - thanks [nruzzu](https://github.com/nruzzu) +* Fix: base64 decoding of headers with a lowercase b +* Feat: support more name variations for ISO codecs +* Feat: update dependencies - thanks [hatch01](https://github.com/hatch01) +* Feat: use standard serialization based on json_serializable +* Feat: Improve high level API fetch message support + # 2.1.1 * Loosened dependency restrictions a bit upon suggestion from [hpoul](https://github.com/Enough-Software/enough_mail/issues/194) * Added support for Big5, KOI8-r and KOI8-u character encodings diff --git a/pubspec.yaml b/pubspec.yaml index 80cd41ec..3b11d3e2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: enough_mail description: IMAP, POP3 and SMTP for email developers. Choose between a low level and a high level API for mailing. Parse and generate MIME messages. Discover email settings. -version: 2.1.1 +version: 2.1.2 homepage: https://github.com/Enough-Software/enough_mail environment: From 38b0da2d4b6e773b518b3b89dacf0683951282df Mon Sep 17 00:00:00 2001 From: Olaf Groeger Date: Sun, 15 Oct 2023 15:49:46 +0200 Subject: [PATCH 10/26] Prefer the Mailbox pathSeparator over the MailAccount pathSeparator --- lib/src/mail/mail_client.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/src/mail/mail_client.dart b/lib/src/mail/mail_client.dart index 97243779..bd9109a9 100644 --- a/lib/src/mail/mail_client.dart +++ b/lib/src/mail/mail_client.dart @@ -372,7 +372,9 @@ class MailClient { List? firstBoxes; firstBoxes = sortMailboxes(order, mailboxes, keepRemaining: false); final boxes = [...mailboxes]..sort((b1, b2) => b1.path.compareTo(b2.path)); - final separator = _account.incoming.pathSeparator; + final separator = (mailboxes.isNotEmpty) + ? mailboxes.first.pathSeparator + : _account.incoming.pathSeparator; final tree = Tree(null) ..populateFromList( boxes, From e9441c0a7aff9c6edb3a2892221a563a91da0847 Mon Sep 17 00:00:00 2001 From: Robert Virkus Date: Mon, 16 Oct 2023 09:09:29 +0200 Subject: [PATCH 11/26] feat: add identityFlag getter to Mailbox --- lib/src/imap/mailbox.dart | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/lib/src/imap/mailbox.dart b/lib/src/imap/mailbox.dart index 0ac93ab4..46d93646 100644 --- a/lib/src/imap/mailbox.dart +++ b/lib/src/imap/mailbox.dart @@ -229,22 +229,46 @@ class Mailbox { /// mod sequence for this mailbox bool get hasModSequence => highestModSequence != null; + /// Tries to retrieve the identity flag of this mailbox + /// + /// Compare [isSpecialUse], [isInbox], [isDrafts], [isSent], [isJunk], + /// [isTrash], [isArchive]. + MailboxFlag? get identityFlag => flags.firstWhereOrNull((flag) => + flag == MailboxFlag.inbox || + flag == MailboxFlag.drafts || + flag == MailboxFlag.sent || + flag == MailboxFlag.junk || + flag == MailboxFlag.trash || + flag == MailboxFlag.archive); + /// Is this the inbox? + /// + /// Compare [isSpecialUse] and [identityFlag] bool get isInbox => hasFlag(MailboxFlag.inbox); /// Is this the drafts folder? + /// + /// Compare [isSpecialUse] and [identityFlag] bool get isDrafts => hasFlag(MailboxFlag.drafts); /// Is this the sent folder? + /// + /// Compare [isSpecialUse] and [identityFlag] bool get isSent => hasFlag(MailboxFlag.sent); /// Is this the junk folder? + /// + /// Compare [isSpecialUse] and [identityFlag] bool get isJunk => hasFlag(MailboxFlag.junk); /// Is this the trash folder? + /// + /// Compare [isSpecialUse] and [identityFlag] bool get isTrash => hasFlag(MailboxFlag.trash); /// Is this the archive folder? + /// + /// Compare [isSpecialUse] and [identityFlag] bool get isArchive => hasFlag(MailboxFlag.archive); /// Is this a virtual mailbox? @@ -254,8 +278,10 @@ class Mailbox { bool get isVirtual => hasFlag(MailboxFlag.virtual); /// Does this mailbox have a known specific purpose? - bool get isSpecialUse => - isInbox || isDrafts || isSent || isJunk || isTrash || isArchive; + /// + /// Compare [identityFlag], [isInbox], [isDrafts], [isSent], [isJunk], + /// [isTrash], [isArchive]. + bool get isSpecialUse => identityFlag != null; /// Checks of the mailbox has the given flag bool hasFlag(MailboxFlag flag) => flags.contains(flag); @@ -336,6 +362,14 @@ class Mailbox { return '$start$pathSeparator$end'; } } + + @override + int get hashCode => encodedPath.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Mailbox && encodedPath == other.encodedPath; } extension _ListExtension on List { From 4b2f3d158aa52ac80595ef5595cebbe03d0f5bb6 Mon Sep 17 00:00:00 2001 From: Robert Virkus Date: Mon, 16 Oct 2023 09:10:08 +0200 Subject: [PATCH 12/26] feat: add firstWhereOrNull search method for a Tree --- lib/src/mail/tree.dart | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/lib/src/mail/tree.dart b/lib/src/mail/tree.dart index 79fcd9f7..52ae1fac 100644 --- a/lib/src/mail/tree.dart +++ b/lib/src/mail/tree.dart @@ -42,7 +42,10 @@ class Tree { } TreeElement _addChildToParent( - T child, T parent, T Function(T child) getParent) { + T child, + T parent, + T Function(T child) getParent, + ) { var treeElement = locate(parent); if (treeElement == null) { final grandParent = getParent(parent); @@ -59,6 +62,27 @@ class Tree { /// Finds the tree element for the given [value]. TreeElement? locate(T value) => _locate(value, root); + /// Locates a specific value in this tree + T? firstWhereOrNull(bool Function(T value) test) => + _firstWhereOrNullFor(test, root); + + T? _firstWhereOrNullFor(bool Function(T value) test, TreeElement element) { + if (test(element.value)) { + return element.value; + } + final children = element.children; + if (children != null) { + for (final child in children) { + final result = _firstWhereOrNullFor(test, child); + if (result != null) { + return result; + } + } + } + + return null; + } + TreeElement? _locate(T value, TreeElement root) { final children = root.children; if (children == null) { From a1860ae62ef00c3aa7c20039f3d319d6f8a471f7 Mon Sep 17 00:00:00 2001 From: Robert Virkus Date: Mon, 16 Oct 2023 17:02:33 +0200 Subject: [PATCH 13/26] fix: ensure to use correct path separator #226 --- lib/src/mail/mail_client.dart | 90 +++++++++++++++++++++++++---------- 1 file changed, 65 insertions(+), 25 deletions(-) diff --git a/lib/src/mail/mail_client.dart b/lib/src/mail/mail_client.dart index bd9109a9..13780dbc 100644 --- a/lib/src/mail/mail_client.dart +++ b/lib/src/mail/mail_client.dart @@ -357,6 +357,15 @@ class MailClient { if (order != null) { boxes = sortMailboxes(order, boxes); } + if (boxes.isNotEmpty) { + final separator = boxes.first.pathSeparator; + if (separator != _account.incoming.pathSeparator) { + _account = _account.copyWith( + incoming: _account.incoming.copyWith(pathSeparator: separator), + ); + unawaited(_onConfigChanged?.call(_account)); + } + } return boxes; } @@ -365,9 +374,10 @@ class MailClient { /// /// Optionally set [createIntermediate] to false, in case not all intermediate /// folders should be created, if not already present on the server. - Future> listMailboxesAsTree( - {bool createIntermediate = true, - List order = defaultMailboxOrder}) async { + Future> listMailboxesAsTree({ + bool createIntermediate = true, + List order = defaultMailboxOrder, + }) async { final mailboxes = _mailboxes ?? await listMailboxes(); List? firstBoxes; firstBoxes = sortMailboxes(order, mailboxes, keepRemaining: false); @@ -462,8 +472,11 @@ class MailClient { /// with [enableCondStore]. /// /// Optionally specify quick resync parameters with [qresync]. - Future selectMailboxByPath(String path, - {bool enableCondStore = false, QResyncParameters? qresync}) async { + Future selectMailboxByPath( + String path, { + bool enableCondStore = false, + QResyncParameters? qresync, + }) async { var mailboxes = _mailboxes; mailboxes ??= await listMailboxes(); final mailbox = mailboxes.firstWhereOrNull((box) => box.path == path); @@ -482,8 +495,11 @@ class MailClient { /// with [enableCondStore]. /// /// Optionally specify quick resync parameters with [qresync]. - Future selectMailboxByFlag(MailboxFlag flag, - {bool enableCondStore = false, QResyncParameters? qresync}) async { + Future selectMailboxByFlag( + MailboxFlag flag, { + bool enableCondStore = false, + QResyncParameters? qresync, + }) async { var mailboxes = _mailboxes; mailboxes ??= await listMailboxes(); final mailbox = getMailbox(flag, mailboxes); @@ -503,8 +519,10 @@ class MailClient { /// /// Optionally specify quick resync parameters with [qresync] - /// for IMAP servers that support `QRESYNC` only. - Future selectInbox( - {bool enableCondStore = false, QResyncParameters? qresync}) async { + Future selectInbox({ + bool enableCondStore = false, + QResyncParameters? qresync, + }) async { var mailboxes = _mailboxes; mailboxes ??= await listMailboxes(); var inbox = mailboxes.firstWhereOrNull((box) => box.isInbox); @@ -523,8 +541,11 @@ class MailClient { /// enabled with [enableCondStore]. /// /// Optionally specify quick resync parameters with [qresync]. - Future selectMailbox(Mailbox mailbox, - {bool enableCondStore = false, QResyncParameters? qresync}) async { + Future selectMailbox( + Mailbox mailbox, { + bool enableCondStore = false, + QResyncParameters? qresync, + }) async { final box = await _incomingMailClient.selectMailbox(mailbox, enableCondStore: enableCondStore, qresync: qresync); _selectedMailbox = box; @@ -857,8 +878,12 @@ class MailClient { return Future.wait(futures); } - Future _sendMessageViaOutgoing(MimeMessage message, MailAddress? from, - bool use8BitEncoding, List? recipients) async { + Future _sendMessageViaOutgoing( + MimeMessage message, + MailAddress? from, + bool use8BitEncoding, + List? recipients, + ) async { await _outgoingMailClient.sendMessage(message, from: from, use8BitEncoding: use8BitEncoding, recipients: recipients); await _outgoingMailClient.disconnect(); @@ -869,8 +894,10 @@ class MailClient { /// /// Optionally specify the [draftsMailbox] when the mail system does not /// support mailbox flags. - Future saveDraftMessage(MimeMessage message, - {Mailbox? draftsMailbox}) { + Future saveDraftMessage( + MimeMessage message, { + Mailbox? draftsMailbox, + }) { if (draftsMailbox == null) { return appendMessageToFlag(message, MailboxFlag.drafts, flags: [MessageFlags.draft, MessageFlags.seen]); @@ -884,8 +911,10 @@ class MailClient { /// /// Optionally specify the message [flags]. Future appendMessageToFlag( - MimeMessage message, MailboxFlag targetMailboxFlag, - {List? flags}) { + MimeMessage message, + MailboxFlag targetMailboxFlag, { + List? flags, + }) { final mailbox = getMailbox(targetMailboxFlag); if (mailbox == null) { throw MailException( @@ -898,8 +927,10 @@ class MailClient { /// /// Optionally specify the message [flags]. Future appendMessage( - MimeMessage message, Mailbox targetMailbox, - {List? flags}) => + MimeMessage message, + Mailbox targetMailbox, { + List? flags, + }) => _incomingMailClient.appendMessage(message, targetMailbox, flags); /// Starts listening for new incoming messages. @@ -1445,8 +1476,11 @@ abstract class _IncomingMailClient { Future> listMailboxes(); - Future selectMailbox(Mailbox mailbox, - {bool enableCondStore = false, QResyncParameters? qresync}); + Future selectMailbox( + Mailbox mailbox, { + bool enableCondStore = false, + QResyncParameters? qresync, + }); Future fetchThreads( Mailbox mailbox, @@ -1885,8 +1919,11 @@ class _IncomingImapClient extends _IncomingMailClient { } @override - Future selectMailbox(Mailbox mailbox, - {bool enableCondStore = false, final QResyncParameters? qresync}) async { + Future selectMailbox( + Mailbox mailbox, { + bool enableCondStore = false, + final QResyncParameters? qresync, + }) async { await _pauseIdle(); try { if (_selectedMailbox != null) { @@ -2849,8 +2886,11 @@ class _IncomingPopClient extends _IncomingMailClient { Future> listMailboxes() => Future.value([_popInbox]); @override - Future selectMailbox(Mailbox mailbox, - {bool enableCondStore = false, QResyncParameters? qresync}) async { + Future selectMailbox( + Mailbox mailbox, { + bool enableCondStore = false, + QResyncParameters? qresync, + }) async { if (mailbox != _popInbox) { throw MailException(mailClient, 'Unknown mailbox $mailbox'); } From 3a6e518c04b07e7aafe37dc36e0773b459b26dcf Mon Sep 17 00:00:00 2001 From: Robert Virkus Date: Mon, 16 Oct 2023 17:03:25 +0200 Subject: [PATCH 14/26] chore: release v2.1.3 --- CHANGELOG.md | 5 +++++ pubspec.yaml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 549266bf..6f4d59f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# 2.1.3 +* Fix: Apply correct mailbox path separator - thanks [nruzzu](https://github.com/nruzzu)! +* Feat: add firstWhereOrNull search method for a Tree +* Feat: add identityFlag getter to Mailbox + # 2.1.2 * Fix: RangeError when a Mailbox name contains a parentheses - thanks [nruzzu](https://github.com/nruzzu) * Fix: base64 decoding of headers with a lowercase b diff --git a/pubspec.yaml b/pubspec.yaml index 3b11d3e2..13df8793 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: enough_mail description: IMAP, POP3 and SMTP for email developers. Choose between a low level and a high level API for mailing. Parse and generate MIME messages. Discover email settings. -version: 2.1.2 +version: 2.1.3 homepage: https://github.com/Enough-Software/enough_mail environment: From 1e39ea1e48980db868d08f84c05c3d1bc4a42323 Mon Sep 17 00:00:00 2001 From: Robert Virkus Date: Sat, 21 Oct 2023 09:14:48 +0200 Subject: [PATCH 15/26] fix: fix array access when no messages where fetched after exists event --- lib/src/mail/mail_client.dart | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/lib/src/mail/mail_client.dart b/lib/src/mail/mail_client.dart index 13780dbc..36e84ba2 100644 --- a/lib/src/mail/mail_client.dart +++ b/lib/src/mail/mail_client.dart @@ -1670,23 +1670,18 @@ class _IncomingImapClient extends _IncomingMailClient { ? FetchPreference.fullWhenWithinSize : FetchPreference.envelope, ); - final last = messages.last; - final messageUid = last.uid; - final mailboxNextUid = _selectedMailbox?.uidNext; - if (mailboxNextUid != null && - messageUid != null && - mailboxNextUid <= messageUid) { - _selectedMailbox?.uidNext = messageUid + 1; - } - for (final message in messages) { - mailClient._fireEvent(MailLoadEvent(message, mailClient)); - _fetchMessages.add(message); - } if (messages.isNotEmpty) { - final lastUid = messages.last.uid; - final selectedMailbox = _selectedMailbox; - if (lastUid != null && selectedMailbox != null) { - selectedMailbox.uidNext = lastUid + 1; + final last = messages.last; + final messageUid = last.uid; + final mailboxNextUid = _selectedMailbox?.uidNext; + if (mailboxNextUid != null && + messageUid != null && + mailboxNextUid <= messageUid) { + _selectedMailbox?.uidNext = messageUid + 1; + } + for (final message in messages) { + mailClient._fireEvent(MailLoadEvent(message, mailClient)); + _fetchMessages.add(message); } } break; From 6b2b7547d47a5086dac1464a8426e69792b94c7e Mon Sep 17 00:00:00 2001 From: Robert Virkus Date: Sun, 22 Oct 2023 20:28:16 +0200 Subject: [PATCH 16/26] feat: synchronize access to incoming low level mail operations in MailClient --- lib/src/mail/mail_client.dart | 451 +++++++++++++++++++++++++--------- pubspec.yaml | 8 +- 2 files changed, 335 insertions(+), 124 deletions(-) diff --git a/lib/src/mail/mail_client.dart b/lib/src/mail/mail_client.dart index 36e84ba2..0b8b0899 100644 --- a/lib/src/mail/mail_client.dart +++ b/lib/src/mail/mail_client.dart @@ -4,6 +4,7 @@ import 'dart:math'; import 'package:collection/collection.dart' show IterableExtension; import 'package:event_bus/event_bus.dart'; +import 'package:synchronized/synchronized.dart'; import '../../enough_mail.dart'; import '../private/util/client_base.dart'; @@ -229,6 +230,7 @@ class MailClient { late _IncomingMailClient _incomingMailClient; late _OutgoingMailClient _outgoingMailClient; + final _incomingLock = Lock(); /// Adds the specified mail event [filter]. /// @@ -352,7 +354,9 @@ class MailClient { /// Optionally specify the [order] of the mailboxes, matching ones will be /// served in the given order. Future> listMailboxes({List? order}) async { - var boxes = await _incomingMailClient.listMailboxes(); + var boxes = await _incomingLock.synchronized( + () => _incomingMailClient.listMailboxes(), + ); _mailboxes = boxes; if (order != null) { boxes = sortMailboxes(order, boxes); @@ -366,6 +370,7 @@ class MailClient { unawaited(_onConfigChanged?.call(_account)); } } + return boxes; } @@ -411,11 +416,14 @@ class MailClient { } TreeElement? _extractTreeElementWithoutChildren( - TreeElement root, Mailbox mailbox) { + TreeElement root, + Mailbox mailbox, + ) { if (root.value == mailbox) { if ((root.children?.isEmpty ?? true) && (root.parent != null)) { root.parent!.children!.remove(root); } + return root as TreeElement?; } if (root.children != null) { @@ -426,6 +434,7 @@ class MailClient { } } } + return null; } @@ -434,6 +443,7 @@ class MailClient { /// When no boxes are given, then the `MailClient.mailboxes` are used. Mailbox? getMailbox(MailboxFlag flag, [List? boxes]) { boxes ??= mailboxes; + return boxes?.firstWhereOrNull((box) => box.hasFlag(flag)); } @@ -446,8 +456,12 @@ class MailClient { /// Set [sortRemainingAlphabetically] to `false` (defaults to `true`) to /// sort the remaining boxes by name, /// is only relevant when [keepRemaining] is `true`. - List sortMailboxes(List order, List mailboxes, - {bool keepRemaining = true, bool sortRemainingAlphabetically = true}) { + List sortMailboxes( + List order, + List mailboxes, { + bool keepRemaining = true, + bool sortRemainingAlphabetically = true, + }) { final inputMailboxes = [...mailboxes]; final outputMailboxes = []; for (final flag in order) { @@ -463,6 +477,7 @@ class MailClient { } outputMailboxes.addAll(inputMailboxes); } + return outputMailboxes; } @@ -483,9 +498,16 @@ class MailClient { if (mailbox == null) { throw MailException(this, 'Unknown mailbox with path <$path>'); } - final box = await _incomingMailClient.selectMailbox(mailbox, - enableCondStore: enableCondStore, qresync: qresync); + final box = await _incomingLock.synchronized( + () => _incomingMailClient.selectMailbox( + mailbox, + enableCondStore: enableCondStore, + qresync: qresync, + ), + ); + _selectedMailbox = box; + return box; } @@ -506,9 +528,15 @@ class MailClient { if (mailbox == null) { throw MailException(this, 'Unknown mailbox with flag <$flag>'); } - final box = await _incomingMailClient.selectMailbox(mailbox, - enableCondStore: enableCondStore, qresync: qresync); + final box = await _incomingLock.synchronized( + () => _incomingMailClient.selectMailbox( + mailbox, + enableCondStore: enableCondStore, + qresync: qresync, + ), + ); _selectedMailbox = box; + return box; } @@ -531,8 +559,12 @@ class MailClient { if (inbox == null) { throw MailException(this, 'Unable to find inbox'); } - return selectMailbox(inbox, - enableCondStore: enableCondStore, qresync: qresync); + + return selectMailbox( + inbox, + enableCondStore: enableCondStore, + qresync: qresync, + ); } /// Selects the specified [mailbox]/folder. @@ -546,9 +578,15 @@ class MailClient { bool enableCondStore = false, QResyncParameters? qresync, }) async { - final box = await _incomingMailClient.selectMailbox(mailbox, - enableCondStore: enableCondStore, qresync: qresync); + final box = await _incomingLock.synchronized( + () => _incomingMailClient.selectMailbox( + mailbox, + enableCondStore: enableCondStore, + qresync: qresync, + ), + ); _selectedMailbox = box; + return box; } @@ -586,9 +624,12 @@ class MailClient { } final sequence = MessageSequence.fromPage(page, count, mailbox.messagesExists); - return _incomingMailClient.fetchMessageSequence( - sequence, - fetchPreference: fetchPreference, + + return _incomingLock.synchronized( + () => _incomingMailClient.fetchMessageSequence( + sequence, + fetchPreference: fetchPreference, + ), ); } @@ -620,8 +661,13 @@ class MailClient { if (mailbox != _selectedMailbox) { await selectMailbox(mailbox); } - return _incomingMailClient.fetchMessageSequence(sequence, - fetchPreference: fetchPreference, markAsSeen: markAsSeen); + return _incomingLock.synchronized( + () => _incomingMailClient.fetchMessageSequence( + sequence, + fetchPreference: fetchPreference, + markAsSeen: markAsSeen, + ), + ); } /// Loads the next page of messages in the given [pagedSequence]. @@ -645,8 +691,11 @@ class MailClient { }) { if (pagedSequence.hasNext) { final sequence = pagedSequence.next(); - return fetchMessageSequence(sequence, - fetchPreference: fetchPreference, markAsSeen: markAsSeen); + return fetchMessageSequence( + sequence, + fetchPreference: fetchPreference, + markAsSeen: markAsSeen, + ); } else { return Future.value([]); } @@ -675,14 +724,19 @@ class MailClient { bool markAsSeen = false, List? includedInlineTypes, Duration? responseTimeout, - }) => - _incomingMailClient.fetchMessageContents( + }) { + _incomingMailClient.log('fetch message contents of ${message.uid}'); + + return _incomingLock.synchronized( + () => _incomingMailClient.fetchMessageContents( message, maxSize: maxSize, markAsSeen: markAsSeen, includedInlineTypes: includedInlineTypes, responseTimeout: responseTimeout, - ); + ), + ); + } /// Fetches the part with the specified [fetchId] of the specified [message]. /// @@ -698,8 +752,13 @@ class MailClient { String fetchId, { Duration? responseTimeout, }) => - _incomingMailClient.fetchMessagePart(message, fetchId, - responseTimeout: responseTimeout); + _incomingLock.synchronized( + () => _incomingMailClient.fetchMessagePart( + message, + fetchId, + responseTimeout: responseTimeout, + ), + ); /// Retrieves the threads starting at [since]. /// @@ -725,13 +784,21 @@ class MailClient { int pageSize = 30, Duration? responseTimeout, }) { - mailbox ??= _selectedMailbox; - if (mailbox == null) { + final usedMailbox = mailbox ?? _selectedMailbox; + if (usedMailbox == null) { throw InvalidArgumentException('no mailbox defined nor selected'); } - return _incomingMailClient.fetchThreads( - mailbox, since, threadPreference, fetchPreference, pageSize, - responseTimeout: responseTimeout); + + return _incomingLock.synchronized( + () => _incomingMailClient.fetchThreads( + usedMailbox, + since, + threadPreference, + fetchPreference, + pageSize, + responseTimeout: responseTimeout, + ), + ); } /// Retrieves the next page for the given [threadResult] @@ -743,9 +810,12 @@ class MailClient { Future> fetchThreadsNextPage( ThreadResult threadResult, ) async { - final messages = await fetchMessagesNextPage(threadResult.threadSequence, - fetchPreference: threadResult.fetchPreference); + final messages = await fetchMessagesNextPage( + threadResult.threadSequence, + fetchPreference: threadResult.fetchPreference, + ); threadResult.addAll(messages); + return messages; } @@ -764,12 +834,18 @@ class MailClient { Mailbox? mailbox, bool setThreadSequences = false, }) { - mailbox ??= _selectedMailbox; - if (mailbox == null) { + final usedMailbox = mailbox ?? _selectedMailbox; + if (usedMailbox == null) { throw InvalidArgumentException('no mailbox defined nor selected'); } - return _incomingMailClient.fetchThreadData(mailbox, since, - setThreadSequences: setThreadSequences); + + return _incomingLock.synchronized( + () => _incomingMailClient.fetchThreadData( + usedMailbox, + since, + setThreadSequences: setThreadSequences, + ), + ); } /// Builds the mime message from the given [messageBuilder] @@ -781,6 +857,7 @@ class MailClient { messageBuilder.setRecommendedTextEncoding( supports8BitMessages: supports8Bit, ); + return messageBuilder.buildMimeMessage(); } @@ -826,9 +903,15 @@ class MailClient { 'sent found in $mailboxes'); } else { futures.add( - appendMessage(message, sentMailbox, flags: [MessageFlags.seen])); + appendMessage( + message, + sentMailbox, + flags: [MessageFlags.seen], + ), + ); } } + return Future.wait(futures); } @@ -872,9 +955,15 @@ class MailClient { 'flag sent found in $mailboxes'); } else { futures.add( - appendMessage(message, sentMailbox, flags: [MessageFlags.seen])); + appendMessage( + message, + sentMailbox, + flags: [MessageFlags.seen], + ), + ); } } + return Future.wait(futures); } @@ -884,8 +973,12 @@ class MailClient { bool use8BitEncoding, List? recipients, ) async { - await _outgoingMailClient.sendMessage(message, - from: from, use8BitEncoding: use8BitEncoding, recipients: recipients); + await _outgoingMailClient.sendMessage( + message, + from: from, + use8BitEncoding: use8BitEncoding, + recipients: recipients, + ); await _outgoingMailClient.disconnect(); } @@ -899,11 +992,17 @@ class MailClient { Mailbox? draftsMailbox, }) { if (draftsMailbox == null) { - return appendMessageToFlag(message, MailboxFlag.drafts, - flags: [MessageFlags.draft, MessageFlags.seen]); + return appendMessageToFlag( + message, + MailboxFlag.drafts, + flags: [MessageFlags.draft, MessageFlags.seen], + ); } else { - return appendMessage(message, draftsMailbox, - flags: [MessageFlags.draft, MessageFlags.seen]); + return appendMessage( + message, + draftsMailbox, + flags: [MessageFlags.draft, MessageFlags.seen], + ); } } @@ -918,8 +1017,11 @@ class MailClient { final mailbox = getMailbox(targetMailboxFlag); if (mailbox == null) { throw MailException( - this, 'No mailbox with flag $targetMailboxFlag found in $mailboxes.'); + this, + 'No mailbox with flag $targetMailboxFlag found in $mailboxes.', + ); } + return appendMessage(message, mailbox, flags: flags); } @@ -931,23 +1033,30 @@ class MailClient { Mailbox targetMailbox, { List? flags, }) => - _incomingMailClient.appendMessage(message, targetMailbox, flags); + _incomingLock.synchronized( + () => _incomingMailClient.appendMessage(message, targetMailbox, flags), + ); /// Starts listening for new incoming messages. /// /// Listen for [MailLoadEvent] on the [eventBus] to get notified /// about new messages. Future startPolling([Duration duration = defaultPollingDuration]) => - _incomingMailClient.startPolling(duration); + _incomingLock.synchronized( + () => _incomingMailClient.startPolling(duration), + ); /// Stops listening for new messages. - Future stopPolling() => _incomingMailClient.stopPolling(); + Future stopPolling() => _incomingLock.synchronized( + () => _incomingMailClient.stopPolling(), + ); /// Stops listening for new messages if this client is currently polling. Future stopPollingIfNeeded() { if (_incomingMailClient.isPolling()) { - return _incomingMailClient.stopPolling(); + return stopPolling(); } + return Future.value(); } @@ -961,22 +1070,27 @@ class MailClient { /// be started again when an error occurred. Future resume({bool startPollingWhenError = true}) async { _incomingMailClient.log('resume mail client'); - try { - await stopPolling(); - await startPolling(); - } catch (e, s) { - _incomingMailClient.log('error while resuming: $e $s'); - // re-connect explicitly: - try { - await _incomingMailClient.reconnect(); - if (startPollingWhenError && !_incomingMailClient.isPolling()) { - await startPolling(); + await _incomingLock.synchronized( + () async { + try { + await _incomingMailClient.stopPolling(); + await _incomingMailClient.startPolling(defaultPollingDuration); + } catch (e, s) { + _incomingMailClient.log('error while resuming: $e $s'); + // re-connect explicitly: + try { + await _incomingMailClient.reconnect(); + if (startPollingWhenError && !_incomingMailClient.isPolling()) { + await _incomingMailClient.startPolling(defaultPollingDuration); + } + } catch (e2, s2) { + _incomingMailClient.log( + 'error while trying to reconnect in resume: $e2 $s2', + ); + } } - } catch (e2, s2) { - _incomingMailClient - .log('error while trying to reconnect in resume: $e2 $s2'); - } - } + }, + ); } /// Determines if message flags such as `\Seen` can be stored. @@ -1000,8 +1114,11 @@ class MailClient { MessageSequence sequence, { int? unchangedSinceModSequence, }) => - store(sequence, [MessageFlags.seen], - unchangedSinceModSequence: unchangedSinceModSequence); + store( + sequence, + [MessageFlags.seen], + unchangedSinceModSequence: unchangedSinceModSequence, + ); /// Mark the messages from the specified [sequence] as unseen/unread. /// @@ -1016,9 +1133,12 @@ class MailClient { MessageSequence sequence, { int? unchangedSinceModSequence, }) => - store(sequence, [MessageFlags.seen], - action: StoreAction.remove, - unchangedSinceModSequence: unchangedSinceModSequence); + store( + sequence, + [MessageFlags.seen], + action: StoreAction.remove, + unchangedSinceModSequence: unchangedSinceModSequence, + ); /// Mark the messages from the specified [sequence] as flagged. /// @@ -1033,8 +1153,11 @@ class MailClient { MessageSequence sequence, { int? unchangedSinceModSequence, }) => - store(sequence, [MessageFlags.flagged], - unchangedSinceModSequence: unchangedSinceModSequence); + store( + sequence, + [MessageFlags.flagged], + unchangedSinceModSequence: unchangedSinceModSequence, + ); /// Mark the messages from the specified [sequence] as unflagged. /// @@ -1049,9 +1172,12 @@ class MailClient { MessageSequence sequence, { int? unchangedSinceModSequence, }) => - store(sequence, [MessageFlags.flagged], - action: StoreAction.remove, - unchangedSinceModSequence: unchangedSinceModSequence); + store( + sequence, + [MessageFlags.flagged], + action: StoreAction.remove, + unchangedSinceModSequence: unchangedSinceModSequence, + ); /// Mark the messages from the specified [sequence] as deleted. /// @@ -1066,8 +1192,11 @@ class MailClient { MessageSequence sequence, { int? unchangedSinceModSequence, }) => - store(sequence, [MessageFlags.deleted], - unchangedSinceModSequence: unchangedSinceModSequence); + store( + sequence, + [MessageFlags.deleted], + unchangedSinceModSequence: unchangedSinceModSequence, + ); /// Mark the messages from the specified [sequence] as not deleted. /// @@ -1082,9 +1211,12 @@ class MailClient { MessageSequence sequence, { int? unchangedSinceModSequence, }) => - store(sequence, [MessageFlags.deleted], - action: StoreAction.remove, - unchangedSinceModSequence: unchangedSinceModSequence); + store( + sequence, + [MessageFlags.deleted], + action: StoreAction.remove, + unchangedSinceModSequence: unchangedSinceModSequence, + ); /// Mark the messages from the specified [sequence] as answered. /// @@ -1099,8 +1231,11 @@ class MailClient { MessageSequence sequence, { int? unchangedSinceModSequence, }) => - store(sequence, [MessageFlags.answered], - unchangedSinceModSequence: unchangedSinceModSequence); + store( + sequence, + [MessageFlags.answered], + unchangedSinceModSequence: unchangedSinceModSequence, + ); /// Mark the messages from the specified [sequence] as not answered. /// @@ -1115,9 +1250,12 @@ class MailClient { MessageSequence sequence, { int? unchangedSinceModSequence, }) => - store(sequence, [MessageFlags.answered], - action: StoreAction.remove, - unchangedSinceModSequence: unchangedSinceModSequence); + store( + sequence, + [MessageFlags.answered], + action: StoreAction.remove, + unchangedSinceModSequence: unchangedSinceModSequence, + ); /// Mark from the specified [sequence] as forwarded. /// @@ -1134,8 +1272,11 @@ class MailClient { bool? silent, int? unchangedSinceModSequence, }) => - store(sequence, [MessageFlags.keywordForwarded], - unchangedSinceModSequence: unchangedSinceModSequence); + store( + sequence, + [MessageFlags.keywordForwarded], + unchangedSinceModSequence: unchangedSinceModSequence, + ); /// Mark the messages from the specified [sequence] as not forwarded. /// @@ -1151,9 +1292,12 @@ class MailClient { MessageSequence sequence, { int? unchangedSinceModSequence, }) => - store(sequence, [MessageFlags.keywordForwarded], - action: StoreAction.remove, - unchangedSinceModSequence: unchangedSinceModSequence); + store( + sequence, + [MessageFlags.keywordForwarded], + action: StoreAction.remove, + unchangedSinceModSequence: unchangedSinceModSequence, + ); /// Flags the [message] with the specified flags. /// @@ -1196,6 +1340,7 @@ class MailClient { if (msgFlags != null) { final sequence = MessageSequence.fromMessage(message); final flags = [...msgFlags]..remove(MessageFlags.recent); + return store(sequence, flags, action: StoreAction.replace); } else { throw MailException(this, 'No message flags defined'); @@ -1219,8 +1364,14 @@ class MailClient { StoreAction action = StoreAction.add, int? unchangedSinceModSequence, }) => - _incomingMailClient.store( - sequence, flags, action, unchangedSinceModSequence); + _incomingLock.synchronized( + () => _incomingMailClient.store( + sequence, + flags, + action, + unchangedSinceModSequence, + ), + ); /// Deletes the given [message]. /// @@ -1259,11 +1410,14 @@ class MailClient { List? messages, }) { final trashMailbox = getMailbox(MailboxFlag.trash); - return _incomingMailClient.deleteMessages( - sequence, - trashMailbox, - expunge: expunge, - messages: messages, + + return _incomingLock.synchronized( + () => _incomingMailClient.deleteMessages( + sequence, + trashMailbox, + expunge: expunge, + messages: messages, + ), ); } @@ -1276,7 +1430,9 @@ class MailClient { /// /// Compare [deleteMessages] Future undoDeleteMessages(DeleteResult deleteResult) => - _incomingMailClient.undoDeleteMessages(deleteResult); + _incomingLock.synchronized( + () => _incomingMailClient.undoDeleteMessages(deleteResult), + ); /// Deletes all messages from the specified [mailbox]. /// @@ -1287,12 +1443,14 @@ class MailClient { Mailbox mailbox, { bool expunge = false, }) async { - final result = - await _incomingMailClient.deleteAllMessages(mailbox, expunge: expunge); + final result = await _incomingLock.synchronized( + () => _incomingMailClient.deleteAllMessages(mailbox, expunge: expunge), + ); mailbox ..messagesExists = 0 ..messagesRecent = 0 ..messagesUnseen = 0; + return result; } @@ -1351,17 +1509,26 @@ class MailClient { throw InvalidArgumentException( 'Move target mailbox with flag $flag not found'); } - return _incomingMailClient.moveMessages(sequence, target, - messages: messages); + return _incomingLock.synchronized( + () => _incomingMailClient.moveMessages( + sequence, + target, + messages: messages, + ), + ); } /// Moves the specified [message] to the given [target] folder /// /// The message UID will be updated automatically. Future moveMessage(MimeMessage message, Mailbox target) => - _incomingMailClient.moveMessages( - MessageSequence.fromMessage(message), target, - messages: [message]); + _incomingLock.synchronized( + () => _incomingMailClient.moveMessages( + MessageSequence.fromMessage(message), + target, + messages: [message], + ), + ); /// Moves the specified message [sequence] to the given [target] folder /// @@ -1371,7 +1538,13 @@ class MailClient { Mailbox target, { List? messages, }) => - _incomingMailClient.moveMessages(sequence, target, messages: messages); + _incomingLock.synchronized( + () => _incomingMailClient.moveMessages( + sequence, + target, + messages: messages, + ), + ); /// Reverts the previous move operation, if possible. /// @@ -1379,14 +1552,18 @@ class MailClient { /// operation, then the UIDs of those messages will be adjusted /// automatically. Future undoMoveMessages(MoveResult moveResult) => - _incomingMailClient.undoMove(moveResult); + _incomingLock.synchronized( + () => _incomingMailClient.undoMove(moveResult), + ); /// Searches the messages with the criteria defined in [search]. /// /// Compare [searchMessagesNextPage] for retrieving the next page /// of search results. Future searchMessages(MailSearch search) => - _incomingMailClient.searchMessages(search); + _incomingLock.synchronized( + () => _incomingMailClient.searchMessages(search), + ); /// Retrieves the next page of messages for the specified [searchResult]. Future> searchMessagesNextPage( @@ -1399,6 +1576,7 @@ class MailClient { final messages = await fetchMessagesNextPage(pagedResult.pagedSequence, fetchPreference: pagedResult.fetchPreference); pagedResult.insertAll(messages); + return messages; } @@ -1413,15 +1591,25 @@ class MailClient { /// /// Specify a [parentMailbox] in case the mailbox should /// not be created in the root. - Future createMailbox(String mailboxName, - {Mailbox? parentMailbox}) async { + Future createMailbox( + String mailboxName, { + Mailbox? parentMailbox, + }) async { if (!supportsMailboxes) { throw MailException( - this, 'Mailboxes are not supported, check "supportsMailboxes" first'); + this, + 'Mailboxes are not supported, check "supportsMailboxes" first', + ); } - final box = await _incomingMailClient.createMailbox(mailboxName, - parentMailbox: parentMailbox); + + final box = await _incomingLock.synchronized( + () => _incomingMailClient.createMailbox( + mailboxName, + parentMailbox: parentMailbox, + ), + ); _mailboxes?.add(box); + return box; } @@ -1429,9 +1617,13 @@ class MailClient { Future deleteMailbox(Mailbox mailbox) async { if (!supportsMailboxes) { throw MailException( - this, 'Mailboxes are not supported, check "supportsMailboxes" first'); + this, + 'Mailboxes are not supported, check "supportsMailboxes" first', + ); } - await _incomingMailClient.deleteMailbox(mailbox); + await _incomingLock.synchronized( + () => _incomingMailClient.deleteMailbox(mailbox), + ); _mailboxes?.remove(mailbox); } } @@ -1713,6 +1905,7 @@ class _IncomingImapClient extends _IncomingMailClient { Future _pauseIdle() { if (_isInIdleMode && !_isIdlePaused) { + _imapClient.log('pause idle...'); _isIdlePaused = true; return stopPolling(); } @@ -1721,6 +1914,7 @@ class _IncomingImapClient extends _IncomingMailClient { Future _resumeIdle() async { if (_isIdlePaused) { + _imapClient.log('resume idle...'); await startPolling(_pollDuration); _isIdlePaused = false; } @@ -2140,8 +2334,10 @@ class _IncomingImapClient extends _IncomingMailClient { } @override - Future startPolling(Duration duration, - {Future Function()? pollImplementation}) async { + Future startPolling( + Duration duration, { + Future Function()? pollImplementation, + }) async { var pollDuration = duration; if (_supportsIdle) { // IMAP Idle timeout is 30 minutes, so official recommendation is to @@ -2153,6 +2349,7 @@ class _IncomingImapClient extends _IncomingMailClient { } pollImplementation ??= _restartIdlePolling; _isInIdleMode = true; + _imapClient.log('start polling...'); try { await _imapClient.idleStart(); } catch (e, s) { @@ -2168,6 +2365,7 @@ class _IncomingImapClient extends _IncomingMailClient { @override Future stopPolling() async { if (_isInIdleMode) { + _imapClient.log('stop polling...'); _isInIdleMode = false; try { await _imapClient.idleDone(); @@ -2186,6 +2384,7 @@ class _IncomingImapClient extends _IncomingMailClient { Future _restartIdlePolling() async { try { + _imapClient.log('restart IDLE...'); //print('restart IDLE...'); await _imapClient.idleDone(); await _imapClient.idleStart(); @@ -2239,12 +2438,18 @@ class _IncomingImapClient extends _IncomingMailClient { try { await _pauseIdle(); final fetchResult = sequence.isUidSequence - ? await _imapClient.uidFetchMessages(sequence, '(BODYSTRUCTURE)', + ? await _imapClient.uidFetchMessages( + sequence, + '(BODYSTRUCTURE)', responseTimeout: - responseTimeout ?? _imapClient.defaultResponseTimeout) - : await _imapClient.fetchMessages(sequence, '(BODYSTRUCTURE)', + responseTimeout ?? _imapClient.defaultResponseTimeout, + ) + : await _imapClient.fetchMessages( + sequence, + '(BODYSTRUCTURE)', responseTimeout: - responseTimeout ?? _imapClient.defaultResponseTimeout); + responseTimeout ?? _imapClient.defaultResponseTimeout, + ); if (fetchResult.messages.isNotEmpty) { final lastMessage = fetchResult.messages.last; if (lastMessage.mediaType.top == MediaToptype.multipart) { @@ -2254,6 +2459,7 @@ class _IncomingImapClient extends _IncomingMailClient { } } } on ImapException catch (e, s) { + await _resumeIdle(); throw MailException.fromImap(mailClient, e, s); } } @@ -2329,8 +2535,11 @@ class _IncomingImapClient extends _IncomingMailClient { await _resumeIdle(); } } - throw MailException(mailClient, - 'Unable to download message with UID ${message.uid} / sequence ID ${message.sequenceId}'); + throw MailException( + mailClient, + 'Unable to download message with UID ${message.uid} / ' + 'sequence ID ${message.sequenceId}', + ); } @override diff --git a/pubspec.yaml b/pubspec.yaml index 13df8793..6e0771f2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,7 @@ name: enough_mail -description: IMAP, POP3 and SMTP for email developers. Choose between a low level and a high level API for mailing. Parse and generate MIME messages. Discover email settings. +description: IMAP, POP3 and SMTP for email developers. Choose between a low + level and a high level API for mailing. Parse and generate MIME messages. + Discover email settings. version: 2.1.3 homepage: https://github.com/Enough-Software/enough_mail @@ -16,8 +18,8 @@ dependencies: intl: ^0.18.0 json_annotation: ^4.8.0 pointycastle: ^3.6.0 - xml: '>=6.0.0 <7.0.0' - + synchronized: ^3.1.0 + xml: ">=6.0.0 <7.0.0" dev_dependencies: build_runner: ^2.3.0 From 71668c07b0e0a68f24ebe8e3a06d44030d3dbd49 Mon Sep 17 00:00:00 2001 From: Robert Virkus Date: Mon, 23 Oct 2023 19:05:16 +0200 Subject: [PATCH 17/26] chore: improve code style --- lib/src/mail/mail_client.dart | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/src/mail/mail_client.dart b/lib/src/mail/mail_client.dart index 0b8b0899..a34fb372 100644 --- a/lib/src/mail/mail_client.dart +++ b/lib/src/mail/mail_client.dart @@ -1069,9 +1069,9 @@ class MailClient { /// Set the [startPollingWhenError] to `false` in case polling should not /// be started again when an error occurred. Future resume({bool startPollingWhenError = true}) async { - _incomingMailClient.log('resume mail client'); await _incomingLock.synchronized( () async { + _incomingMailClient.log('resume mail client'); try { await _incomingMailClient.stopPolling(); await _incomingMailClient.startPolling(defaultPollingDuration); @@ -2125,10 +2125,14 @@ class _IncomingImapClient extends _IncomingMailClient { quickReSync = QResyncParameters(mailbox.uidValidity, mailbox.highestModSequence); } - final selectedMailbox = await _imapClient.selectMailbox(mailbox, - enableCondStore: enableCondStore, qresync: quickReSync); + final selectedMailbox = await _imapClient.selectMailbox( + mailbox, + enableCondStore: enableCondStore, + qresync: quickReSync, + ); _selectedMailbox = selectedMailbox; _threadData = null; + return selectedMailbox; } on ImapException catch (e) { throw MailException.fromImap(mailClient, e); @@ -2269,6 +2273,7 @@ class _IncomingImapClient extends _IncomingMailClient { mailboxUidValidity: mailboxUidValidity, ); } + return fetchImapResult.messages; } @@ -2282,6 +2287,7 @@ class _IncomingImapClient extends _IncomingMailClient { if (_fetchMessages.isEmpty) { return []; } + return _fetchMessages.toList(); } on ImapException catch (e) { throw MailException.fromImap(mailClient, e); @@ -2321,6 +2327,7 @@ class _IncomingImapClient extends _IncomingMailClient { mailClient, 'Unable to fetch message part <$fetchId>'); } message.setPart(fetchId, part); + return part; } else { throw MailException( @@ -2358,6 +2365,7 @@ class _IncomingImapClient extends _IncomingMailClient { // throw MailException.fromImap(mailClient, e); } } + return super .startPolling(pollDuration, pollImplementation: pollImplementation); } From 405539a0dc851b07679915ab9dcd86e192eff355 Mon Sep 17 00:00:00 2001 From: Robert Virkus Date: Thu, 26 Oct 2023 15:58:43 +0200 Subject: [PATCH 18/26] fix: limit the number of fetched messages in extreme EXISTS cases --- lib/src/imap/imap_events.dart | 12 ++++++---- lib/src/mail/mail_client.dart | 19 ++++++++++++--- lib/src/private/imap/generic_parser.dart | 20 ++++++++++++---- lib/src/private/imap/noop_parser.dart | 30 +++++++++++++++++------- lib/src/private/imap/select_parser.dart | 4 +++- 5 files changed, 64 insertions(+), 21 deletions(-) diff --git a/lib/src/imap/imap_events.dart b/lib/src/imap/imap_events.dart index 7c1727a5..28cb0eb8 100644 --- a/lib/src/imap/imap_events.dart +++ b/lib/src/imap/imap_events.dart @@ -89,8 +89,10 @@ class ImapFetchEvent extends ImapEvent { class ImapMessagesExistEvent extends ImapEvent { /// Creates a new IMAP event ImapMessagesExistEvent( - this.newMessagesExists, this.oldMessagesExists, ImapClient imapClient) - : super(ImapEventType.exists, imapClient); + this.newMessagesExists, + this.oldMessagesExists, + ImapClient imapClient, + ) : super(ImapEventType.exists, imapClient); /// The current number of existing messages final int newMessagesExists; @@ -103,8 +105,10 @@ class ImapMessagesExistEvent extends ImapEvent { class ImapMessagesRecentEvent extends ImapEvent { /// Creates a new IMAP event ImapMessagesRecentEvent( - this.newMessagesRecent, this.oldMessagesRecent, ImapClient imapClient) - : super(ImapEventType.recent, imapClient); + this.newMessagesRecent, + this.oldMessagesRecent, + ImapClient imapClient, + ) : super(ImapEventType.recent, imapClient); /// The current number of recent messages final int newMessagesRecent; diff --git a/lib/src/mail/mail_client.dart b/lib/src/mail/mail_client.dart index a34fb372..fe7fdacc 100644 --- a/lib/src/mail/mail_client.dart +++ b/lib/src/mail/mail_client.dart @@ -1852,7 +1852,18 @@ class _IncomingImapClient extends _IncomingMailClient { } final sequence = MessageSequence(); if (evt.newMessagesExists - evt.oldMessagesExists > 1) { - sequence.addRange(evt.oldMessagesExists, evt.newMessagesExists); + final oldMessagesExists = + evt.oldMessagesExists == 0 ? 1 : evt.oldMessagesExists; + final range = evt.newMessagesExists - oldMessagesExists; + if (range > 100) { + // this is very unlikely, limit the number of fetched messages: + sequence.addRange( + max(evt.newMessagesExists - 10, 1), + evt.newMessagesExists, + ); + } else { + sequence.addRange(oldMessagesExists, evt.newMessagesExists); + } } else { sequence.add(evt.newMessagesExists); } @@ -2723,8 +2734,10 @@ class _IncomingImapClient extends _IncomingMailClient { } @override - Future deleteAllMessages(Mailbox mailbox, - {bool expunge = false}) async { + Future deleteAllMessages( + Mailbox mailbox, { + bool expunge = false, + }) async { var canUndo = true; final sequence = MessageSequence.fromAll(); final selectedMailbox = _selectedMailbox; diff --git a/lib/src/private/imap/generic_parser.dart b/lib/src/private/imap/generic_parser.dart index c3033df7..0bb0efca 100644 --- a/lib/src/private/imap/generic_parser.dart +++ b/lib/src/private/imap/generic_parser.dart @@ -40,7 +40,9 @@ class GenericParser extends ResponseParser { @override bool parseUntagged( - ImapResponse imapResponse, Response? response) { + ImapResponse imapResponse, + Response? response, + ) { final text = imapResponse.parseText; if (text.startsWith('NO ')) { _result.warnings.add(ImapWarning('NO', text.substring('NO '.length))); @@ -67,7 +69,14 @@ class GenericParser extends ResponseParser { final previous = box.messagesExists; box.messagesExists = exists; unawaited( - _fireDelayed(ImapMessagesExistEvent(exists, previous, imapClient))); + _fireDelayed( + ImapMessagesExistEvent( + exists, + previous, + imapClient, + ), + ), + ); } return true; } else if (text.endsWith('RECENT')) { @@ -78,8 +87,11 @@ class GenericParser extends ResponseParser { final recent = parseInt(text, 0, ' ') ?? 0; final previous = box.messagesRecent; box.messagesRecent = recent; - unawaited(_fireDelayed( - ImapMessagesRecentEvent(recent, previous, imapClient))); + unawaited( + _fireDelayed( + ImapMessagesRecentEvent(recent, previous, imapClient), + ), + ); } return true; } diff --git a/lib/src/private/imap/noop_parser.dart b/lib/src/private/imap/noop_parser.dart index 7e4e0825..94baeab3 100644 --- a/lib/src/private/imap/noop_parser.dart +++ b/lib/src/private/imap/noop_parser.dart @@ -62,11 +62,21 @@ class NoopParser extends ResponseParser { if (handled) { if (box.messagesExists != messagesExists) { - imapClient.eventBus.fire(ImapMessagesExistEvent( - box.messagesExists, messagesExists, imapClient)); + imapClient.eventBus.fire( + ImapMessagesExistEvent( + box.messagesExists, + messagesExists, + imapClient, + ), + ); } else if (box.messagesRecent != messagesRecent) { - imapClient.eventBus.fire(ImapMessagesRecentEvent( - box.messagesRecent, messagesRecent, imapClient)); + imapClient.eventBus.fire( + ImapMessagesRecentEvent( + box.messagesRecent, + messagesRecent, + imapClient, + ), + ); } return true; } else { @@ -75,11 +85,13 @@ class NoopParser extends ResponseParser { if (mimeMessage != null) { imapClient.eventBus.fire(ImapFetchEvent(mimeMessage, imapClient)); } else if (_fetchParser.vanishedMessages != null) { - imapClient.eventBus.fire(ImapVanishedEvent( - _fetchParser.vanishedMessages, - imapClient, - isEarlier: true, - )); + imapClient.eventBus.fire( + ImapVanishedEvent( + _fetchParser.vanishedMessages, + imapClient, + isEarlier: true, + ), + ); } return true; } diff --git a/lib/src/private/imap/select_parser.dart b/lib/src/private/imap/select_parser.dart index 156a2088..9a9b7433 100644 --- a/lib/src/private/imap/select_parser.dart +++ b/lib/src/private/imap/select_parser.dart @@ -55,7 +55,9 @@ class SelectParser extends ResponseParser { /// Helps with parsing untagged responses static bool parseUntaggedResponse( - Mailbox mailbox, ImapResponse imapResponse) { + Mailbox mailbox, + ImapResponse imapResponse, + ) { final box = mailbox; final details = imapResponse.parseText; if (details.startsWith('OK [UNSEEN ')) { From b073b7a1c51afc29ab43e0bb63660c06220a7063 Mon Sep 17 00:00:00 2001 From: Robert Virkus Date: Thu, 26 Oct 2023 17:06:30 +0200 Subject: [PATCH 19/26] chore: improve line breaks / code style --- lib/src/codecs/base64_mail_codec.dart | 1 + lib/src/codecs/mail_codec.dart | 4 +++- lib/src/mail/mail_authentication.dart | 1 + lib/src/mime_data.dart | 28 ++++++++++++++++++--------- 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/lib/src/codecs/base64_mail_codec.dart b/lib/src/codecs/base64_mail_codec.dart index 1554b0f4..6ae56734 100644 --- a/lib/src/codecs/base64_mail_codec.dart +++ b/lib/src/codecs/base64_mail_codec.dart @@ -129,6 +129,7 @@ class Base64MailCodec extends MailCodec { } cleaned = buffer.toString(); } + return base64.decode(cleaned); } diff --git a/lib/src/codecs/mail_codec.dart b/lib/src/codecs/mail_codec.dart index 8cf3762a..8f47832e 100644 --- a/lib/src/codecs/mail_codec.dart +++ b/lib/src/codecs/mail_codec.dart @@ -281,7 +281,9 @@ abstract class MailCodec { /// Decodes the given binary [text] static Uint8List decodeBinary( - final String text, final String? transferEncoding) { + final String text, + final String? transferEncoding, + ) { final tEncoding = transferEncoding ?? contentTransferEncodingNone; final decoder = _binaryDecodersByName[tEncoding.toLowerCase()]; if (decoder == null) { diff --git a/lib/src/mail/mail_authentication.dart b/lib/src/mail/mail_authentication.dart index a53de2b2..61d7828c 100644 --- a/lib/src/mail/mail_authentication.dart +++ b/lib/src/mail/mail_authentication.dart @@ -151,6 +151,7 @@ class OauthToken { if (json['created'] == null) { json['created'] = DateTime.now().toUtc().toIso8601String(); } + return OauthToken.fromJson(json); } diff --git a/lib/src/mime_data.dart b/lib/src/mime_data.dart index c7af477c..e7474050 100644 --- a/lib/src/mime_data.dart +++ b/lib/src/mime_data.dart @@ -1,6 +1,8 @@ +import 'dart:convert'; import 'dart:typed_data'; import 'package:collection/collection.dart' show IterableExtension; + import 'codecs/mail_codec.dart'; import 'mime_message.dart'; import 'private/imap/parser_helper.dart'; @@ -252,7 +254,9 @@ class BinaryMimeData extends MimeData { } List _splitAndParse( - final String boundaryText, final Uint8List bodyData) { + final String boundaryText, + final Uint8List bodyData, + ) { final boundary = '--$boundaryText\r\n'.codeUnits; final result = []; // end is expected to be \r\n for all but the last one, where -- is expected, possibly followed by \r\n @@ -300,18 +304,22 @@ class BinaryMimeData extends MimeData { } } } + return result; } @override String decodeText( - ContentTypeHeader? contentTypeHeader, String? contentTransferEncoding) { - if (_bodyStartIndex == null) { - return ''; - } - return MailCodec.decodeAsText( - _bodyData, contentTransferEncoding, contentTypeHeader?.charset); - } + ContentTypeHeader? contentTypeHeader, + String? contentTransferEncoding, + ) => + _bodyStartIndex == null + ? '' + : MailCodec.decodeAsText( + _bodyData, + contentTransferEncoding, + contentTypeHeader?.charset, + ); @override Uint8List decodeBinary(String? contentTransferEncoding) { @@ -326,7 +334,8 @@ class BinaryMimeData extends MimeData { // even with a 'binary' content transfer encoding there are \r\n // characters that need to be handled, // so translate to text first - final dataText = String.fromCharCodes(_bodyData); + final dataText = utf8.decode(_bodyData); + return MailCodec.decodeBinary(dataText, contentTransferEncodingLC); } @@ -353,6 +362,7 @@ class BinaryMimeData extends MimeData { } // the whole data is just headers: final headerLines = String.fromCharCodes(headerData).split('\r\n'); + return ParserHelper.parseHeaderLines(headerLines).headersList; } From 6a640f413a5050e91f222d6a387825c8063fea49 Mon Sep 17 00:00:00 2001 From: Robert Virkus Date: Sat, 28 Oct 2023 14:27:02 +0200 Subject: [PATCH 20/26] fix: OAuth: use existing refresh token when no new refresh token is delivered --- lib/src/mail/mail_authentication.dart | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/src/mail/mail_authentication.dart b/lib/src/mail/mail_authentication.dart index 61d7828c..46b75f15 100644 --- a/lib/src/mail/mail_authentication.dart +++ b/lib/src/mail/mail_authentication.dart @@ -143,11 +143,18 @@ class OauthToken { _$OauthTokenFromJson(json); /// Parses a new token from the given [text]. - factory OauthToken.fromText(String text, {String? provider}) { + factory OauthToken.fromText( + String text, { + String? provider, + String? refreshToken, + }) { final json = jsonDecode(text); if (provider != null) { json['provider'] = provider; } + if (refreshToken != null && json['refresh_token'] == null) { + json['refresh_token'] = refreshToken; + } if (json['created'] == null) { json['created'] = DateTime.now().toUtc().toIso8601String(); } From bdea46925cb27a52c26162af1b8ead7851952185 Mon Sep 17 00:00:00 2001 From: Robert Virkus Date: Mon, 30 Oct 2023 18:37:01 +0100 Subject: [PATCH 21/26] fix: handle XOAUTH2 continuation in case of sign error --- lib/src/imap/imap_client.dart | 479 +++++++++++++++++++++--------- lib/src/private/imap/command.dart | 28 +- 2 files changed, 353 insertions(+), 154 deletions(-) diff --git a/lib/src/imap/imap_client.dart b/lib/src/imap/imap_client.dart index d3243c91..2bd27bc1 100644 --- a/lib/src/imap/imap_client.dart +++ b/lib/src/imap/imap_client.dart @@ -346,12 +346,16 @@ class ImapClient extends ClientBase { /// /// Note that the capability 'AUTH=XOAUTH2' needs to be present. Future> authenticateWithOAuth2( - String user, String accessToken) async { + String user, + String accessToken, + ) async { final authText = 'user=$user\u{0001}auth=Bearer $accessToken\u{0001}\u{0001}'; final authBase64Text = base64.encode(utf8.encode(authText)); - final cmd = Command( - 'AUTHENTICATE XOAUTH2 $authBase64Text', + // the empty client response to a challenge yields the actual server + // error message response + final cmd = Command.withContinuation( + ['AUTHENTICATE XOAUTH2 $authBase64Text', ''], logText: 'AUTHENTICATE XOAUTH2 (base64 code scrambled)', writeTimeout: defaultWriteTimeout, responseTimeout: defaultResponseTimeout, @@ -370,8 +374,11 @@ class ImapClient extends ClientBase { /// Note that the capability 'AUTH=OAUTHBEARER' needs to be present. /// Compare https://tools.ietf.org/html/rfc7628 for details Future> authenticateWithOAuthBearer( - String user, String accessToken, - {String? host, int? port}) async { + String user, + String accessToken, { + String? host, + int? port, + }) async { host ??= serverInfo.host; port ??= serverInfo.port; final authText = 'n,u=$user,\u{0001}' @@ -458,10 +465,17 @@ class ImapClient extends ClientBase { /// Compare [selectMailbox], [selectMailboxByPath] or [selectInbox] for /// selecting a mailbox first. /// Compare [uidCopy] for the copying files based on their sequence IDs - Future copy(MessageSequence sequence, - {Mailbox? targetMailbox, String? targetMailboxPath}) => - _copyOrMove('COPY', sequence, - targetMailbox: targetMailbox, targetMailboxPath: targetMailboxPath); + Future copy( + MessageSequence sequence, { + Mailbox? targetMailbox, + String? targetMailboxPath, + }) => + _copyOrMove( + 'COPY', + sequence, + targetMailbox: targetMailbox, + targetMailboxPath: targetMailboxPath, + ); /// Copies the specified message(s) from the specified [sequence] /// from the currently selected mailbox to the target mailbox. @@ -472,10 +486,17 @@ class ImapClient extends ClientBase { /// Compare [selectMailbox], [selectMailboxByPath] or [selectInbox] for /// selecting a mailbox first. /// Compare [copy] for the version with message sequence IDs - Future uidCopy(MessageSequence sequence, - {Mailbox? targetMailbox, String? targetMailboxPath}) => - _copyOrMove('UID COPY', sequence, - targetMailbox: targetMailbox, targetMailboxPath: targetMailboxPath); + Future uidCopy( + MessageSequence sequence, { + Mailbox? targetMailbox, + String? targetMailboxPath, + }) => + _copyOrMove( + 'UID COPY', + sequence, + targetMailbox: targetMailbox, + targetMailboxPath: targetMailboxPath, + ); /// Moves the specified message(s) from the specified [sequence] /// from the currently selected mailbox to the target mailbox. @@ -487,14 +508,21 @@ class ImapClient extends ClientBase { /// Compare [uidMove] for moving messages based on their UID /// The move command is only available for servers that advertise the /// `MOVE` capability. - Future move(MessageSequence sequence, - {Mailbox? targetMailbox, String? targetMailboxPath}) { + Future move( + MessageSequence sequence, { + Mailbox? targetMailbox, + String? targetMailboxPath, + }) { if (targetMailbox == null && targetMailboxPath == null) { throw InvalidArgumentException( 'move() error: Neither targetMailbox nor targetMailboxPath defined.'); } - return _copyOrMove('MOVE', sequence, - targetMailbox: targetMailbox, targetMailboxPath: targetMailboxPath); + return _copyOrMove( + 'MOVE', + sequence, + targetMailbox: targetMailbox, + targetMailboxPath: targetMailboxPath, + ); } /// Copies the specified message(s) from the specified [sequence] @@ -505,20 +533,30 @@ class ImapClient extends ClientBase { /// Compare [selectMailbox], [selectMailboxByPath] or [selectInbox] for /// selecting a mailbox first. /// Compare [copy] for the version with message sequence IDs - Future uidMove(MessageSequence sequence, - {Mailbox? targetMailbox, String? targetMailboxPath}) { + Future uidMove( + MessageSequence sequence, { + Mailbox? targetMailbox, + String? targetMailboxPath, + }) { if (targetMailbox == null && targetMailboxPath == null) { throw InvalidArgumentException('uidMove() error: Neither targetMailbox ' 'nor targetMailboxPath defined.'); } - return _copyOrMove('UID MOVE', sequence, - targetMailbox: targetMailbox, targetMailboxPath: targetMailboxPath); + return _copyOrMove( + 'UID MOVE', + sequence, + targetMailbox: targetMailbox, + targetMailboxPath: targetMailboxPath, + ); } /// Implementation for both COPY or MOVE Future _copyOrMove( - String command, MessageSequence sequence, - {Mailbox? targetMailbox, String? targetMailboxPath}) { + String command, + MessageSequence sequence, { + Mailbox? targetMailbox, + String? targetMailboxPath, + }) { final selectedMailbox = _selectedMailbox; if (selectedMailbox == null) { throw InvalidArgumentException('No mailbox selected.'); @@ -558,14 +596,22 @@ class ImapClient extends ClientBase { /// selecting a mailbox first. /// Compare the methods [markSeen], [markFlagged], etc for typical store /// operations. - Future store(MessageSequence sequence, List flags, - {StoreAction? action, - bool? silent, - int? unchangedSinceModSequence}) => - _store(false, 'STORE', sequence, flags, - action: action, - silent: silent, - unchangedSinceModSequence: unchangedSinceModSequence); + Future store( + MessageSequence sequence, + List flags, { + StoreAction? action, + bool? silent, + int? unchangedSinceModSequence, + }) => + _store( + false, + 'STORE', + sequence, + flags, + action: action, + silent: silent, + unchangedSinceModSequence: unchangedSinceModSequence, + ); /// Updates the [flags] of the message(s) from the specified [sequence] /// in the currently selected mailbox. @@ -585,21 +631,33 @@ class ImapClient extends ClientBase { /// selecting a mailbox first. /// Compare the methods [uidMarkSeen], [uidMarkFlagged], etc for typical /// store operations. - Future uidStore(MessageSequence sequence, List flags, - {StoreAction? action, - bool? silent, - int? unchangedSinceModSequence}) => - _store(true, 'UID STORE', sequence, flags, - action: action, - silent: silent, - unchangedSinceModSequence: unchangedSinceModSequence); + Future uidStore( + MessageSequence sequence, + List flags, { + StoreAction? action, + bool? silent, + int? unchangedSinceModSequence, + }) => + _store( + true, + 'UID STORE', + sequence, + flags, + action: action, + silent: silent, + unchangedSinceModSequence: unchangedSinceModSequence, + ); /// STORE and UID STORE implementation - Future _store(bool isUidStore, String command, - MessageSequence sequence, List flags, - {StoreAction? action, - bool? silent, - int? unchangedSinceModSequence}) async { + Future _store( + bool isUidStore, + String command, + MessageSequence sequence, + List flags, { + StoreAction? action, + bool? silent, + int? unchangedSinceModSequence, + }) async { if (_selectedMailbox == null) { throw InvalidArgumentException('No mailbox selected.'); } @@ -648,6 +706,7 @@ class ImapClient extends ClientBase { final result = StoreImapResult() ..changedMessages = messagesResponse.messages ..modifiedMessageSequence = messagesResponse.modifiedSequence; + return result; } @@ -660,10 +719,17 @@ class ImapClient extends ClientBase { /// `QRESYNC` capability /// Compare the [store] method in case you need more control or want to /// change several flags. - Future markSeen(MessageSequence sequence, - {bool? silent, int? unchangedSinceModSequence}) => - store(sequence, [MessageFlags.seen], - silent: silent, unchangedSinceModSequence: unchangedSinceModSequence); + Future markSeen( + MessageSequence sequence, { + bool? silent, + int? unchangedSinceModSequence, + }) => + store( + sequence, + [MessageFlags.seen], + silent: silent, + unchangedSinceModSequence: unchangedSinceModSequence, + ); /// Mark the messages from the specified [sequence] as unseen/unread. /// @@ -674,12 +740,18 @@ class ImapClient extends ClientBase { /// `QRESYNC` capability /// Compare the [store] method in case you need more control or want to /// change several flags. - Future markUnseen(MessageSequence sequence, - {bool? silent, int? unchangedSinceModSequence}) => - store(sequence, [MessageFlags.seen], - action: StoreAction.remove, - silent: silent, - unchangedSinceModSequence: unchangedSinceModSequence); + Future markUnseen( + MessageSequence sequence, { + bool? silent, + int? unchangedSinceModSequence, + }) => + store( + sequence, + [MessageFlags.seen], + action: StoreAction.remove, + silent: silent, + unchangedSinceModSequence: unchangedSinceModSequence, + ); /// Mark the messages from the specified [sequence] as flagged. /// @@ -690,10 +762,17 @@ class ImapClient extends ClientBase { /// `QRESYNC` capability /// Compare the [store] method in case you need more control or want to /// change several flags. - Future markFlagged(MessageSequence sequence, - {bool? silent, int? unchangedSinceModSequence}) => - store(sequence, [MessageFlags.flagged], - silent: silent, unchangedSinceModSequence: unchangedSinceModSequence); + Future markFlagged( + MessageSequence sequence, { + bool? silent, + int? unchangedSinceModSequence, + }) => + store( + sequence, + [MessageFlags.flagged], + silent: silent, + unchangedSinceModSequence: unchangedSinceModSequence, + ); /// Mark the messages from the specified [sequence] as unflagged. /// @@ -704,12 +783,18 @@ class ImapClient extends ClientBase { /// `CONDSTORE` or `QRESYNC` capability /// Compare the [store] method in case you need more control or want to /// change several flags. - Future markUnflagged(MessageSequence sequence, - {bool? silent, int? unchangedSinceModSequence}) => - store(sequence, [MessageFlags.flagged], - action: StoreAction.remove, - silent: silent, - unchangedSinceModSequence: unchangedSinceModSequence); + Future markUnflagged( + MessageSequence sequence, { + bool? silent, + int? unchangedSinceModSequence, + }) => + store( + sequence, + [MessageFlags.flagged], + action: StoreAction.remove, + silent: silent, + unchangedSinceModSequence: unchangedSinceModSequence, + ); /// Mark the messages from the specified [sequence] as deleted. /// @@ -720,10 +805,17 @@ class ImapClient extends ClientBase { /// `CONDSTORE` or `QRESYNC` capability /// Compare the [store] method in case you need more control or want to /// change several flags. - Future markDeleted(MessageSequence sequence, - {bool? silent, int? unchangedSinceModSequence}) => - store(sequence, [MessageFlags.deleted], - silent: silent, unchangedSinceModSequence: unchangedSinceModSequence); + Future markDeleted( + MessageSequence sequence, { + bool? silent, + int? unchangedSinceModSequence, + }) => + store( + sequence, + [MessageFlags.deleted], + silent: silent, + unchangedSinceModSequence: unchangedSinceModSequence, + ); /// Mark the messages from the specified [sequence] as not deleted. /// @@ -734,12 +826,18 @@ class ImapClient extends ClientBase { /// `CONDSTORE` or `QRESYNC` capability /// Compare the [store] method in case you need more control or want to /// change several flags. - Future markUndeleted(MessageSequence sequence, - {bool? silent, int? unchangedSinceModSequence}) => - store(sequence, [MessageFlags.deleted], - action: StoreAction.remove, - silent: silent, - unchangedSinceModSequence: unchangedSinceModSequence); + Future markUndeleted( + MessageSequence sequence, { + bool? silent, + int? unchangedSinceModSequence, + }) => + store( + sequence, + [MessageFlags.deleted], + action: StoreAction.remove, + silent: silent, + unchangedSinceModSequence: unchangedSinceModSequence, + ); /// Mark the messages from the specified [sequence] as answered. /// @@ -750,10 +848,17 @@ class ImapClient extends ClientBase { /// `CONDSTORE` or `QRESYNC` capability /// Compare the [store] method in case you need more control or want to /// change several flags. - Future markAnswered(MessageSequence sequence, - {bool? silent, int? unchangedSinceModSequence}) => - store(sequence, [MessageFlags.answered], - silent: silent, unchangedSinceModSequence: unchangedSinceModSequence); + Future markAnswered( + MessageSequence sequence, { + bool? silent, + int? unchangedSinceModSequence, + }) => + store( + sequence, + [MessageFlags.answered], + silent: silent, + unchangedSinceModSequence: unchangedSinceModSequence, + ); /// Mark the messages from the specified [sequence] as not answered. /// @@ -764,12 +869,18 @@ class ImapClient extends ClientBase { /// `CONDSTORE` or `QRESYNC` capability /// Compare the [store] method in case you need more control or want to /// change several flags. - Future markUnanswered(MessageSequence sequence, - {bool? silent, int? unchangedSinceModSequence}) => - store(sequence, [MessageFlags.answered], - action: StoreAction.remove, - silent: silent, - unchangedSinceModSequence: unchangedSinceModSequence); + Future markUnanswered( + MessageSequence sequence, { + bool? silent, + int? unchangedSinceModSequence, + }) => + store( + sequence, + [MessageFlags.answered], + action: StoreAction.remove, + silent: silent, + unchangedSinceModSequence: unchangedSinceModSequence, + ); /// Mark the messages from the specified [sequence] as forwarded. /// @@ -781,10 +892,17 @@ class ImapClient extends ClientBase { /// `CONDSTORE` or `QRESYNC` capability /// Compare the [store] method in case you need more control or want to /// change several flags. - Future markForwarded(MessageSequence sequence, - {bool? silent, int? unchangedSinceModSequence}) => - store(sequence, [MessageFlags.keywordForwarded], - silent: silent, unchangedSinceModSequence: unchangedSinceModSequence); + Future markForwarded( + MessageSequence sequence, { + bool? silent, + int? unchangedSinceModSequence, + }) => + store( + sequence, + [MessageFlags.keywordForwarded], + silent: silent, + unchangedSinceModSequence: unchangedSinceModSequence, + ); /// Mark the messages from the specified [sequence] as not forwarded. /// @@ -796,12 +914,18 @@ class ImapClient extends ClientBase { /// `CONDSTORE` or `QRESYNC` capability /// Compare the [store] method in case you need more control or want to /// change several flags. - Future markUnforwarded(MessageSequence sequence, - {bool? silent, int? unchangedSinceModSequence}) => - store(sequence, [MessageFlags.keywordForwarded], - action: StoreAction.remove, - silent: silent, - unchangedSinceModSequence: unchangedSinceModSequence); + Future markUnforwarded( + MessageSequence sequence, { + bool? silent, + int? unchangedSinceModSequence, + }) => + store( + sequence, + [MessageFlags.keywordForwarded], + action: StoreAction.remove, + silent: silent, + unchangedSinceModSequence: unchangedSinceModSequence, + ); /// Mark the messages from the specified [sequence] as seen/read. /// @@ -812,10 +936,17 @@ class ImapClient extends ClientBase { /// `CONDSTORE` or `QRESYNC` capability /// Compare the [uidStore] method in case you need more control or want to /// change several flags. - Future uidMarkSeen(MessageSequence sequence, - {bool? silent, int? unchangedSinceModSequence}) => - uidStore(sequence, [MessageFlags.seen], - silent: silent, unchangedSinceModSequence: unchangedSinceModSequence); + Future uidMarkSeen( + MessageSequence sequence, { + bool? silent, + int? unchangedSinceModSequence, + }) => + uidStore( + sequence, + [MessageFlags.seen], + silent: silent, + unchangedSinceModSequence: unchangedSinceModSequence, + ); /// Mark the messages from the specified [sequence] as unseen/unread. /// @@ -826,12 +957,18 @@ class ImapClient extends ClientBase { /// `CONDSTORE` or `QRESYNC` capability /// Compare the [uidStore] method in case you need more control or want to /// change several flags. - Future uidMarkUnseen(MessageSequence sequence, - {bool? silent, int? unchangedSinceModSequence}) => - uidStore(sequence, [MessageFlags.seen], - action: StoreAction.remove, - silent: silent, - unchangedSinceModSequence: unchangedSinceModSequence); + Future uidMarkUnseen( + MessageSequence sequence, { + bool? silent, + int? unchangedSinceModSequence, + }) => + uidStore( + sequence, + [MessageFlags.seen], + action: StoreAction.remove, + silent: silent, + unchangedSinceModSequence: unchangedSinceModSequence, + ); /// Mark the messages from the specified [sequence] as flagged. /// @@ -842,10 +979,17 @@ class ImapClient extends ClientBase { /// `CONDSTORE` or `QRESYNC` capability /// Compare the [uidStore] method in case you need more control or want to /// change several flags. - Future uidMarkFlagged(MessageSequence sequence, - {bool? silent, int? unchangedSinceModSequence}) => - uidStore(sequence, [MessageFlags.flagged], - silent: silent, unchangedSinceModSequence: unchangedSinceModSequence); + Future uidMarkFlagged( + MessageSequence sequence, { + bool? silent, + int? unchangedSinceModSequence, + }) => + uidStore( + sequence, + [MessageFlags.flagged], + silent: silent, + unchangedSinceModSequence: unchangedSinceModSequence, + ); /// Mark the messages from the specified [sequence] as unflagged. /// @@ -856,12 +1000,18 @@ class ImapClient extends ClientBase { /// `CONDSTORE` or `QRESYNC` capability /// Compare the [uidStore] method in case you need more control or want to /// change several flags. - Future uidMarkUnflagged(MessageSequence sequence, - {bool? silent, int? unchangedSinceModSequence}) => - uidStore(sequence, [MessageFlags.flagged], - action: StoreAction.remove, - silent: silent, - unchangedSinceModSequence: unchangedSinceModSequence); + Future uidMarkUnflagged( + MessageSequence sequence, { + bool? silent, + int? unchangedSinceModSequence, + }) => + uidStore( + sequence, + [MessageFlags.flagged], + action: StoreAction.remove, + silent: silent, + unchangedSinceModSequence: unchangedSinceModSequence, + ); /// Mark the messages from the specified [sequence] as deleted. /// @@ -872,10 +1022,17 @@ class ImapClient extends ClientBase { /// `CONDSTORE` or `QRESYNC` capability /// Compare the [uidStore] method in case you need more control or want to /// change several flags. - Future uidMarkDeleted(MessageSequence sequence, - {bool? silent, int? unchangedSinceModSequence}) => - uidStore(sequence, [MessageFlags.deleted], - silent: silent, unchangedSinceModSequence: unchangedSinceModSequence); + Future uidMarkDeleted( + MessageSequence sequence, { + bool? silent, + int? unchangedSinceModSequence, + }) => + uidStore( + sequence, + [MessageFlags.deleted], + silent: silent, + unchangedSinceModSequence: unchangedSinceModSequence, + ); /// Mark the messages from the specified [sequence] as not deleted. /// @@ -886,12 +1043,18 @@ class ImapClient extends ClientBase { /// `CONDSTORE` or `QRESYNC` capability /// Compare the [uidStore] method in case you need more control or want to /// change several flags. - Future uidMarkUndeleted(MessageSequence sequence, - {bool? silent, int? unchangedSinceModSequence}) => - uidStore(sequence, [MessageFlags.deleted], - action: StoreAction.remove, - silent: silent, - unchangedSinceModSequence: unchangedSinceModSequence); + Future uidMarkUndeleted( + MessageSequence sequence, { + bool? silent, + int? unchangedSinceModSequence, + }) => + uidStore( + sequence, + [MessageFlags.deleted], + action: StoreAction.remove, + silent: silent, + unchangedSinceModSequence: unchangedSinceModSequence, + ); /// Mark the messages from the specified [sequence] as answered. /// @@ -902,10 +1065,17 @@ class ImapClient extends ClientBase { /// `CONDSTORE` or `QRESYNC` capability /// Compare the [uidStore] method in case you need more control or want to /// change several flags. - Future uidMarkAnswered(MessageSequence sequence, - {bool? silent, int? unchangedSinceModSequence}) => - uidStore(sequence, [MessageFlags.answered], - silent: silent, unchangedSinceModSequence: unchangedSinceModSequence); + Future uidMarkAnswered( + MessageSequence sequence, { + bool? silent, + int? unchangedSinceModSequence, + }) => + uidStore( + sequence, + [MessageFlags.answered], + silent: silent, + unchangedSinceModSequence: unchangedSinceModSequence, + ); /// Mark the messages from the specified [sequence] as not answered. /// @@ -916,12 +1086,18 @@ class ImapClient extends ClientBase { /// `CONDSTORE` or `QRESYNC` capability /// Compare the [uidStore] method in case you need more control or want to /// change several flags. - Future uidMarkUnanswered(MessageSequence sequence, - {bool? silent, int? unchangedSinceModSequence}) => - uidStore(sequence, [MessageFlags.answered], - action: StoreAction.remove, - silent: silent, - unchangedSinceModSequence: unchangedSinceModSequence); + Future uidMarkUnanswered( + MessageSequence sequence, { + bool? silent, + int? unchangedSinceModSequence, + }) => + uidStore( + sequence, + [MessageFlags.answered], + action: StoreAction.remove, + silent: silent, + unchangedSinceModSequence: unchangedSinceModSequence, + ); /// Mark the messages from the specified [sequence] as forwarded. /// @@ -933,10 +1109,17 @@ class ImapClient extends ClientBase { /// `CONDSTORE` or `QRESYNC` capability /// Compare the [uidStore] method in case you need more control or want to /// change several flags. - Future uidMarkForwarded(MessageSequence sequence, - {bool? silent, int? unchangedSinceModSequence}) => - uidStore(sequence, [MessageFlags.keywordForwarded], - silent: silent, unchangedSinceModSequence: unchangedSinceModSequence); + Future uidMarkForwarded( + MessageSequence sequence, { + bool? silent, + int? unchangedSinceModSequence, + }) => + uidStore( + sequence, + [MessageFlags.keywordForwarded], + silent: silent, + unchangedSinceModSequence: unchangedSinceModSequence, + ); /// Mark the messages from the specified [sequence] as not forwarded. /// @@ -948,12 +1131,18 @@ class ImapClient extends ClientBase { /// `CONDSTORE` or `QRESYNC` capability /// Compare the [uidStore] method in case you need more control or want to /// change several flags. - Future uidMarkUnforwarded(MessageSequence sequence, - {bool? silent, int? unchangedSinceModSequence}) => - uidStore(sequence, [MessageFlags.keywordForwarded], - action: StoreAction.remove, - silent: silent, - unchangedSinceModSequence: unchangedSinceModSequence); + Future uidMarkUnforwarded( + MessageSequence sequence, { + bool? silent, + int? unchangedSinceModSequence, + }) => + uidStore( + sequence, + [MessageFlags.keywordForwarded], + action: StoreAction.remove, + silent: silent, + unchangedSinceModSequence: unchangedSinceModSequence, + ); /// Trigger a noop (no operation). /// @@ -2386,7 +2575,7 @@ class ImapClient extends ClientBase { } } if (!_isInIdleMode) { - logApp('continuation not handled: [$imapResponse]'); + logApp('continuation not handled: [$imapResponse], current cmd: $cmd'); } } diff --git a/lib/src/private/imap/command.dart b/lib/src/private/imap/command.dart index 4d5c317b..21ca572c 100644 --- a/lib/src/private/imap/command.dart +++ b/lib/src/private/imap/command.dart @@ -7,17 +7,27 @@ import 'response_parser.dart'; /// Contains an IMAP command class Command { /// Creates a new command - Command(this.commandText, - {this.logText, this.parts, this.writeTimeout, this.responseTimeout}); + Command( + this.commandText, { + this.logText, + this.parts, + this.writeTimeout, + this.responseTimeout, + }); /// Creates a new multiline command - Command.withContinuation(List parts, - {String? logText, Duration? writeTimeout, Duration? responseTimeout}) - : this(parts.first, - parts: parts, - logText: logText, - writeTimeout: writeTimeout, - responseTimeout: responseTimeout); + Command.withContinuation( + List parts, { + String? logText, + Duration? writeTimeout, + Duration? responseTimeout, + }) : this( + parts.first, + parts: parts, + logText: logText, + writeTimeout: writeTimeout, + responseTimeout: responseTimeout, + ); /// The command text final String commandText; From 57a1c85656402d57315b5de63e22db91101e3d83 Mon Sep 17 00:00:00 2001 From: Robert Virkus Date: Tue, 31 Oct 2023 21:05:04 +0100 Subject: [PATCH 22/26] chore: improve code style, add error details to ImapException --- lib/src/imap/response.dart | 2 +- lib/src/mail/mail_client.dart | 10 +++++++--- lib/src/private/imap/capability_parser.dart | 16 +++++++++++++--- lib/src/private/imap/command.dart | 11 +++++++++-- lib/src/private/imap/imap_response.dart | 8 ++++++-- lib/src/private/imap/response_parser.dart | 14 ++++++++++---- 6 files changed, 46 insertions(+), 15 deletions(-) diff --git a/lib/src/imap/response.dart b/lib/src/imap/response.dart index c6dbfe04..12dbac94 100644 --- a/lib/src/imap/response.dart +++ b/lib/src/imap/response.dart @@ -10,7 +10,7 @@ enum ResponseStatus { /// The command is supported but the client send a wrong request /// or is a wrong state - bad + bad, } /// Base class for command responses. diff --git a/lib/src/mail/mail_client.dart b/lib/src/mail/mail_client.dart index fe7fdacc..29d8c79c 100644 --- a/lib/src/mail/mail_client.dart +++ b/lib/src/mail/mail_client.dart @@ -284,6 +284,7 @@ class MailClient { if (auth is OauthAuthentication && auth.token.isExpired) { OauthToken? refreshed; try { + _incomingMailClient.log('Refreshing token...'); refreshed = await refresh(this, auth.token); } catch (e, s) { final message = 'Unable to refresh token: $e $s'; @@ -314,7 +315,8 @@ class MailClient { await onConfigChanged(account); } catch (e, s) { _incomingMailClient.log( - 'Unable to handle onConfigChanged $onConfigChanged: $e $s'); + 'Unable to handle onConfigChanged $onConfigChanged: $e $s', + ); } } } @@ -2053,8 +2055,10 @@ class _IncomingImapClient extends _IncomingMailClient { final serverConfig = _config.serverConfig; final isSecure = serverConfig.socketType == SocketType.ssl; await _imapClient.connectToServer( - serverConfig.hostname!, serverConfig.port!, - isSecure: isSecure); + serverConfig.hostname!, + serverConfig.port!, + isSecure: isSecure, + ); if (!isSecure) { if (_imapClient.serverInfo.supportsStartTls && (serverConfig.socketType != SocketType.plainNoStartTls)) { diff --git a/lib/src/private/imap/capability_parser.dart b/lib/src/private/imap/capability_parser.dart index e38596ea..1238556f 100644 --- a/lib/src/private/imap/capability_parser.dart +++ b/lib/src/private/imap/capability_parser.dart @@ -15,21 +15,30 @@ class CapabilityParser extends ResponseParser> { @override List? parse( - ImapResponse imapResponse, Response> response) { + ImapResponse imapResponse, + Response> response, + ) { if (response.isOkStatus) { if (imapResponse.parseText.startsWith('OK [CAPABILITY ')) { parseCapabilities( - imapResponse.first.line!, 'OK [CAPABILITY '.length, info); + imapResponse.first.line!, + 'OK [CAPABILITY '.length, + info, + ); _capabilities = info.capabilities; } + return _capabilities ?? []; } + return null; } @override bool parseUntagged( - ImapResponse imapResponse, Response>? response) { + ImapResponse imapResponse, + Response>? response, + ) { final line = imapResponse.parseText; if (line.startsWith('OK [CAPABILITY ')) { parseCapabilities(line, 'OK [CAPABILITY '.length, info); @@ -40,6 +49,7 @@ class CapabilityParser extends ResponseParser> { _capabilities = info.capabilities; return true; } + return super.parseUntagged(imapResponse, response); } diff --git a/lib/src/private/imap/command.dart b/lib/src/private/imap/command.dart index 21ca572c..4f0032df 100644 --- a/lib/src/private/imap/command.dart +++ b/lib/src/private/imap/command.dart @@ -92,11 +92,18 @@ class CommandTask { if (imapResponse.parseText.startsWith('OK ')) { response.status = ResponseStatus.ok; } else if (imapResponse.parseText.startsWith('NO ')) { - response.status = ResponseStatus.no; + response + ..status = ResponseStatus.no + ..details = imapResponse.parseText.length > 3 + ? imapResponse.parseText.substring(3) + : imapResponse.parseText; } else { - response.status = ResponseStatus.bad; + response + ..status = ResponseStatus.bad + ..details = imapResponse.parseText; } response.result = parser.parse(imapResponse, response); + return response; } diff --git a/lib/src/private/imap/imap_response.dart b/lib/src/private/imap/imap_response.dart index 3391a65e..2f5ca465 100644 --- a/lib/src/private/imap/imap_response.dart +++ b/lib/src/private/imap/imap_response.dart @@ -137,8 +137,10 @@ class ImapResponse { if (current.parent != null) { current = current.parent!; } else { - print('Warning: no parent for closing parentheses, ' - 'last parentheses type $lastType'); + print( + 'Warning: no parent for closing parentheses, ' + 'last parentheses type $lastType', + ); } } else if (char != AsciiRunes.runeSpace) { isInValue = true; @@ -192,8 +194,10 @@ class ImapValueIterator { bool next() { if (_currentIndex < values.length - 1) { _currentIndex++; + return true; } + return false; } } diff --git a/lib/src/private/imap/response_parser.dart b/lib/src/private/imap/response_parser.dart index 414ee7ea..e69a2b57 100644 --- a/lib/src/private/imap/response_parser.dart +++ b/lib/src/private/imap/response_parser.dart @@ -16,15 +16,21 @@ abstract class ResponseParser { /// Helper method to parse list entries in a line [details]. List? parseListEntries( - String details, int startIndex, String? endCharacter, - [String separator = ' ']) => + String details, + int startIndex, + String? endCharacter, [ + String separator = ' ', + ]) => ParserHelper.parseListEntries( details, startIndex, endCharacter, separator); /// Helper method to parse a list of integer values in a line [details]. List? parseListIntEntries( - String details, int startIndex, String endCharacter, - [String separator = ' ']) => + String details, + int startIndex, + String endCharacter, [ + String separator = ' ', + ]) => ParserHelper.parseListIntEntries( details, startIndex, endCharacter, separator); } From 7cea5601c657675178532b85a322403cc835e3ed Mon Sep 17 00:00:00 2001 From: Robert Virkus Date: Sat, 4 Nov 2023 12:56:36 +0100 Subject: [PATCH 23/26] feat: show error details when SMTP OAuth fails, improve refresh handling --- lib/src/imap/imap_client.dart | 9 +- lib/src/mail/mail_authentication.dart | 23 +++- lib/src/mail/mail_client.dart | 106 ++++++++++-------- .../commands/smtp_auth_xoauth2_command.dart | 19 +++- 4 files changed, 104 insertions(+), 53 deletions(-) diff --git a/lib/src/imap/imap_client.dart b/lib/src/imap/imap_client.dart index 2bd27bc1..89069694 100644 --- a/lib/src/imap/imap_client.dart +++ b/lib/src/imap/imap_client.dart @@ -218,18 +218,23 @@ enum StatusFlags { class ImapClient extends ClientBase { /// Creates a new ImapClient instance. /// - /// Set the [eventBus] to add your specific `EventBus` to listen to + /// Set the [bus] to add your specific `EventBus` to listen to /// IMAP events. + /// /// Set [isLogEnabled] to `true` for getting log outputs on the standard /// output. + /// /// Optionally specify a [logName] that is given out at logs to differentiate /// between different imap clients. + /// /// Set the [defaultWriteTimeout] in case the connection connection should /// timeout automatically after the given time. + /// /// [onBadCertificate] is an optional handler for unverifiable certificates. /// The handler receives the [X509Certificate], and can inspect it and decide /// (or let the user decide) whether to accept the connection or not. - /// The handler should return true to continue the [SecureSocket] connection. + /// The handler should return `true` to continue the [SecureSocket] + /// connection. ImapClient({ EventBus? bus, bool isLogEnabled = false, diff --git a/lib/src/mail/mail_authentication.dart b/lib/src/mail/mail_authentication.dart index 46b75f15..c672f4f6 100644 --- a/lib/src/mail/mail_authentication.dart +++ b/lib/src/mail/mail_authentication.dart @@ -170,6 +170,9 @@ class OauthToken { final String accessToken; /// Expiration in seconds from [created] time + /// + /// Compare [expiresDateTime], [willExpireIn] + /// and [isExpired] @JsonKey(name: 'expires_in') final int expiresIn; @@ -193,12 +196,30 @@ class OauthToken { final String? provider; /// Checks if this token is expired + /// + /// Compare [willExpireIn] and [isValid] bool get isExpired => expiresDateTime.isBefore(DateTime.now().toUtc()); + /// Checks if the token is already expired or will expire + /// within the given (positive) [duration]. + bool willExpireIn(Duration duration) { + print( + 'willExpireIn(): \n' + 'expiresDateTime=$expiresDateTime, now=${DateTime.now().toUtc()},\n' + 'duration=$duration, ' + 'compare=${DateTime.now().toUtc().subtract(duration)}', + ); + return expiresDateTime.isBefore(DateTime.now().toUtc().subtract(duration)); + } + /// Retrieves the expiry date time + /// + /// Compare [willExpireIn] DateTime get expiresDateTime => created.add(Duration(seconds: expiresIn)); - /// Checks if this token is still valid, ie not expired + /// Checks if this token is still valid, ie not expired. + /// + /// Compare [isExpired] bool get isValid => !isExpired; /// Refreshes this token with the new [accessToken] and [expiresIn]. diff --git a/lib/src/mail/mail_client.dart b/lib/src/mail/mail_client.dart index 29d8c79c..dc477207 100644 --- a/lib/src/mail/mail_client.dart +++ b/lib/src/mail/mail_client.dart @@ -93,16 +93,26 @@ class MailClient { ); } else if (config.serverConfig.type == ServerType.pop) { _incomingMailClient = _IncomingPopClient( - _downloadSizeLimit, _eventBus, logName, config, this, - isLogEnabled: _isLogEnabled, onBadCertificate: onBadCertificate); + _downloadSizeLimit, + _eventBus, + logName, + config, + this, + isLogEnabled: _isLogEnabled, + onBadCertificate: onBadCertificate, + ); } else { - throw InvalidArgumentException('Unsupported incoming' - 'server type [${config.serverConfig.typeName}].'); + throw InvalidArgumentException( + 'Unsupported incoming' + 'server type [${config.serverConfig.typeName}].', + ); } final outgoingConfig = _account.outgoing; if (outgoingConfig.serverConfig.type != ServerType.smtp) { - print('Warning: unknown outgoing server ' - 'type ${outgoingConfig.serverConfig.typeName}.'); + print( + 'Warning: unknown outgoing server ' + 'type ${outgoingConfig.serverConfig.typeName}.', + ); } _outgoingMailClient = _OutgoingSmtpClient( this, @@ -231,6 +241,7 @@ class MailClient { late _IncomingMailClient _incomingMailClient; late _OutgoingMailClient _outgoingMailClient; final _incomingLock = Lock(); + final _outgoingLock = Lock(); /// Adds the specified mail event [filter]. /// @@ -281,7 +292,8 @@ class MailClient { final refresh = _refreshOAuthToken; if (refresh != null) { final auth = account.incoming.authentication; - if (auth is OauthAuthentication && auth.token.isExpired) { + if (auth is OauthAuthentication && + auth.token.willExpireIn(const Duration(minutes: 15))) { OauthToken? refreshed; try { _incomingMailClient.log('Refreshing token...'); @@ -309,6 +321,8 @@ class MailClient { incoming: incoming, outgoing: outgoing, ); + _incomingMailClient._config = _account.incoming; + _outgoingMailClient._mailConfig = _account.outgoing; final onConfigChanged = _onConfigChanged; if (onConfigChanged != null) { try { @@ -329,21 +343,29 @@ class MailClient { Future disconnect() async { final futures = [ stopPollingIfNeeded(), - _incomingMailClient.disconnect(), - _outgoingMailClient.disconnect(), + _incomingLock.synchronized( + () => _incomingMailClient.disconnect(), + ), + _outgoingLock.synchronized( + () => _outgoingMailClient.disconnect(), + ), ]; _isConnected = false; await Future.wait(futures); } - /// Enforces to reconnect with the service. + /// Enforces to reconnect with the incoming service. /// /// Also compare [disconnect]. /// Also compare [connect]. Future reconnect() async { - await _incomingMailClient.disconnect(); - await _incomingMailClient.reconnect(); - _isConnected = true; + await _incomingLock.synchronized( + () async { + await _incomingMailClient.disconnect(); + await _incomingMailClient.reconnect(); + _isConnected = true; + }, + ); } // Future tryAuthenticate( @@ -880,7 +902,7 @@ class MailClient { /// /// Optionally specify the [sentMailbox] when the mail system does not /// support mailbox flags. - Future sendMessageBuilder( + Future sendMessageBuilder( MessageBuilder messageBuilder, { MailAddress? from, bool appendToSent = true, @@ -894,27 +916,14 @@ class MailClient { final message = messageBuilder.buildMimeMessage(); final use8Bit = builderEncoding == TransferEncoding.eightBit; - final futures = [ - _sendMessageViaOutgoing(message, from, use8Bit, recipients), - ]; - if (appendToSent && _incomingMailClient.supportsAppendingMessages) { - sentMailbox ??= getMailbox(MailboxFlag.sent); - if (sentMailbox == null) { - _incomingMailClient.log( - 'Error: unable to append sent message: no no mailbox with flag ' - 'sent found in $mailboxes'); - } else { - futures.add( - appendMessage( - message, - sentMailbox, - flags: [MessageFlags.seen], - ), - ); - } - } - - return Future.wait(futures); + return sendMessage( + message, + from: from, + appendToSent: appendToSent, + sentMailbox: sentMailbox, + use8BitEncoding: use8Bit, + recipients: recipients, + ); } /// Sends the specified [message]. @@ -945,9 +954,13 @@ class MailClient { Mailbox? sentMailbox, bool use8BitEncoding = false, List? recipients, - }) { + }) async { + await _prepareConnect(); final futures = [ - _sendMessageViaOutgoing(message, from, use8BitEncoding, recipients), + _outgoingLock.synchronized( + () => + _sendMessageViaOutgoing(message, from, use8BitEncoding, recipients), + ), ]; if (appendToSent && _incomingMailClient.supportsAppendingMessages) { sentMailbox ??= getMailbox(MailboxFlag.sent); @@ -966,7 +979,7 @@ class MailClient { } } - return Future.wait(futures); + await Future.wait(futures); } Future _sendMessageViaOutgoing( @@ -3334,18 +3347,24 @@ class _IncomingPopClient extends _IncomingMailClient { } abstract class _OutgoingMailClient { + _OutgoingMailClient({required MailServerConfig mailConfig}) + : _mailConfig = mailConfig; + ClientBase get client; ServerType get clientType; + MailServerConfig _mailConfig; /// Checks if the incoming mail client supports 8 bit encoded messages. /// /// Is only correct after authorizing. Future supports8BitEncoding(); - Future sendMessage(MimeMessage message, - {MailAddress? from, - bool use8BitEncoding = false, - List? recipients}); + Future sendMessage( + MimeMessage message, { + MailAddress? from, + bool use8BitEncoding = false, + List? recipients, + }); Future disconnect(); } @@ -3367,7 +3386,7 @@ class _OutgoingSmtpClient extends _OutgoingMailClient { // defaultWriteTimeout: connectionTimeout, onBadCertificate: onBadCertificate, ), - _mailConfig = mailConfig; + super(mailConfig: mailConfig); @override ClientBase get client => _smtpClient; @@ -3375,7 +3394,6 @@ class _OutgoingSmtpClient extends _OutgoingMailClient { ServerType get clientType => ServerType.smtp; final MailClient mailClient; final SmtpClient _smtpClient; - final MailServerConfig _mailConfig; Future _connectOutgoingIfRequired() async { if (!_smtpClient.isLoggedIn) { diff --git a/lib/src/private/smtp/commands/smtp_auth_xoauth2_command.dart b/lib/src/private/smtp/commands/smtp_auth_xoauth2_command.dart index 80a6bafd..996c4f32 100644 --- a/lib/src/private/smtp/commands/smtp_auth_xoauth2_command.dart +++ b/lib/src/private/smtp/commands/smtp_auth_xoauth2_command.dart @@ -11,7 +11,7 @@ class SmtpAuthXOauth2Command extends SmtpCommand { final String? _userName; final String? _accessToken; - bool _authSent = false; + var _authSentSentCounter = 0; @override String get command => 'AUTH XOAUTH2'; @@ -19,12 +19,18 @@ class SmtpAuthXOauth2Command extends SmtpCommand { @override String? nextCommand(SmtpResponse response) { if (response.code != 334 && response.code != 235) { - print('Warning: Unexpected status code during AUTH XOAUTH2: ' - '${response.code}. Expected: 334 or 235. \nauthSent=$_authSent'); + print( + 'Warning: Unexpected status code during AUTH XOAUTH2: ' + '${response.code}. Expected: 334 or 235.\n' + 'authSentCounter=$_authSentSentCounter ', + ); } - if (!_authSent) { - _authSent = true; + if (_authSentSentCounter == 0) { + _authSentSentCounter = 1; return getBase64EncodedData(); + } else if (response.code == 334 && _authSentSentCounter == 1) { + _authSentSentCounter++; + return ''; // send empty line to receive error details } else { return null; } @@ -39,7 +45,8 @@ class SmtpAuthXOauth2Command extends SmtpCommand { } @override - bool isCommandDone(SmtpResponse response) => _authSent; + bool isCommandDone(SmtpResponse response) => + response.code != 334 && _authSentSentCounter > 0; @override String toString() => 'AUTH XOAUTH2 '; From 956f6c438f4f941542178265666bf4c30590ea2e Mon Sep 17 00:00:00 2001 From: Robert Virkus Date: Sat, 11 Nov 2023 11:19:22 +0100 Subject: [PATCH 24/26] fix: oauth expiration check in high level API --- lib/src/mail/mail_authentication.dart | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/lib/src/mail/mail_authentication.dart b/lib/src/mail/mail_authentication.dart index c672f4f6..27a75974 100644 --- a/lib/src/mail/mail_authentication.dart +++ b/lib/src/mail/mail_authentication.dart @@ -201,16 +201,10 @@ class OauthToken { bool get isExpired => expiresDateTime.isBefore(DateTime.now().toUtc()); /// Checks if the token is already expired or will expire - /// within the given (positive) [duration]. - bool willExpireIn(Duration duration) { - print( - 'willExpireIn(): \n' - 'expiresDateTime=$expiresDateTime, now=${DateTime.now().toUtc()},\n' - 'duration=$duration, ' - 'compare=${DateTime.now().toUtc().subtract(duration)}', - ); - return expiresDateTime.isBefore(DateTime.now().toUtc().subtract(duration)); - } + /// within the given (positive) [duration], e.g. + /// `const Duration(minutes: 15)`. + bool willExpireIn(Duration duration) => + expiresDateTime.isBefore(DateTime.now().toUtc().add(duration)); /// Retrieves the expiry date time /// From 0fb729a858f452b329a4b1f5a971461baa7d7fd0 Mon Sep 17 00:00:00 2001 From: Robert Virkus Date: Sat, 11 Nov 2023 11:45:16 +0100 Subject: [PATCH 25/26] chore: simplify implicit mailbox selection when fetching messages Possibly related to: cannot delete message with high level API #228 --- lib/src/mail/mail_client.dart | 45 ++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/lib/src/mail/mail_client.dart b/lib/src/mail/mail_client.dart index dc477207..168f8cdb 100644 --- a/lib/src/mail/mail_client.dart +++ b/lib/src/mail/mail_client.dart @@ -614,6 +614,18 @@ class MailClient { return box; } + Future _selectMailboxIfNeeded(Mailbox? mailbox) { + final usedMailbox = mailbox ?? _selectedMailbox; + if (usedMailbox == null) { + throw MailException(this, 'No mailbox selected'); + } + if (usedMailbox != _selectedMailbox) { + return selectMailbox(usedMailbox); + } + + return Future.value(usedMailbox); + } + /// Loads the specified [page] of messages starting at the latest message /// and going down [count] messages. /// @@ -628,7 +640,7 @@ class MailClient { /// By default messages that are within the size bounds as defined in the /// `downloadSizeLimit` in the `MailClient`s constructor are downloaded fully. /// - /// Note that the preference cannot be realized on some backends such as + /// Note that the [fetchPreference] cannot be realized on some backends such as /// POP3 mail servers. /// /// Compare [fetchMessagesNextPage] @@ -638,16 +650,9 @@ class MailClient { int page = 1, FetchPreference fetchPreference = FetchPreference.fullWhenWithinSize, }) async { - mailbox ??= _selectedMailbox; - if (mailbox == null) { - throw InvalidArgumentException( - 'Either specify a mailbox or select a mailbox first'); - } - if (mailbox != _selectedMailbox) { - await selectMailbox(mailbox); - } + final usedMailbox = await _selectMailboxIfNeeded(mailbox); final sequence = - MessageSequence.fromPage(page, count, mailbox.messagesExists); + MessageSequence.fromPage(page, count, usedMailbox.messagesExists); return _incomingLock.synchronized( () => _incomingMailClient.fetchMessageSequence( @@ -677,14 +682,8 @@ class MailClient { FetchPreference fetchPreference = FetchPreference.fullWhenWithinSize, bool markAsSeen = false, }) async { - mailbox ??= _selectedMailbox; - if (mailbox == null) { - throw InvalidArgumentException( - 'Either specify a mailbox or select a mailbox first'); - } - if (mailbox != _selectedMailbox) { - await selectMailbox(mailbox); - } + await _selectMailboxIfNeeded(mailbox); + return _incomingLock.synchronized( () => _incomingMailClient.fetchMessageSequence( sequence, @@ -705,24 +704,26 @@ class MailClient { /// Set [markAsSeen] to `true` to automatically add the `\Seen` flag in case /// it is not there yet when downloading the `fetchPreference.full`. /// - /// Note that the preference cannot be realized on some backends such as + /// Note that the [fetchPreference] cannot be realized on some backends such as /// POP3 mail servers. Future> fetchMessagesNextPage( PagedMessageSequence pagedSequence, { Mailbox? mailbox, FetchPreference fetchPreference = FetchPreference.fullWhenWithinSize, bool markAsSeen = false, - }) { + }) async { if (pagedSequence.hasNext) { final sequence = pagedSequence.next(); + return fetchMessageSequence( sequence, + mailbox: mailbox, fetchPreference: fetchPreference, markAsSeen: markAsSeen, ); - } else { - return Future.value([]); } + + return Future.value([]); } /// Fetches the contents of the specified [message]. From 83fcefbd4e615b497dca33bd13676394e29c8c6b Mon Sep 17 00:00:00 2001 From: Robert Virkus Date: Sat, 11 Nov 2023 11:53:07 +0100 Subject: [PATCH 26/26] chore: release v2.1.4 --- CHANGELOG.md | 8 ++++++++ pubspec.yaml | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f4d59f4..1cfdf011 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# 2.1.4 +* Fix: use refreshed OAUTH tokens when using the high level MailClient API. +* Fix: handle edge cases in IMAP `FETCH` responses. +* Feat: Add details for low level IMAP errors when using the high level MailCLient API. +* Feat: Refresh OAUTH tokens 15 minutes in advance before they expire to reduce the risk of a token expiring during a long running operation. +* Feat: show error details when SMTP XOAuth2 authentication fails. +* Feat: synchronize access to low level clients when using the high level MailClient API. + # 2.1.3 * Fix: Apply correct mailbox path separator - thanks [nruzzu](https://github.com/nruzzu)! * Feat: add firstWhereOrNull search method for a Tree diff --git a/pubspec.yaml b/pubspec.yaml index 6e0771f2..dfa66eba 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: enough_mail description: IMAP, POP3 and SMTP for email developers. Choose between a low level and a high level API for mailing. Parse and generate MIME messages. Discover email settings. -version: 2.1.3 +version: 2.1.4 homepage: https://github.com/Enough-Software/enough_mail environment: