diff --git a/.vscode/settings.json b/.vscode/settings.json index 259a93eb..23cc1211 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -82,6 +82,7 @@ "unflagged", "Unforwarded", "UNKEYWORD", + "Unselectable", "writeln", "xoauth" ], diff --git a/README.md b/README.md index 906c4ad7..99482166 100644 --- a/README.md +++ b/README.md @@ -314,6 +314,9 @@ The following SMTP extensions are supported: The following security extensions are supported: * ✅ Partial signing of messages using [DKIM](https://tools.ietf.org/html/rfc6376) +### Other +* ✅ [Mailto](https://tools.ietf.org/html/rfc6068) parsing mailto links +* ✅ [Email provider auto-discovery](https://tools.ietf.org/html/rfc6186) Discover settings for an email address ### Supported encodings Character encodings: diff --git a/analysis_options.yaml b/analysis_options.yaml index f57ba05b..6eecbf36 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -4,6 +4,29 @@ analyzer: todo: ignore plugins: - dart_code_metrics # https://github.com/dart-code-checker/dart-code-metrics + exclude: + - '**/*.g.*' + +dart_code_metrics: + anti-patterns: + - long-method + - long-parameter-list + metrics: + cyclomatic-complexity: 20 + maximum-nesting-level: 7 + number-of-parameters: 10 + source-lines-of-code: 100 + metrics-exclude: + - test/** + rules: + - newline-before-return + - no-boolean-literal-compare + - no-empty-block + - prefer-trailing-comma + - prefer-conditional-expressions + - no-equal-then-else + - avoid-non-null-assertion + linter: rules: - always_declare_return_types @@ -148,15 +171,4 @@ linter: - use_to_and_as_if_applicable - valid_regexps - void_checks -dart_code_metrics: - metrics: - # disable metrics - cyclomatic-complexity: 50 - maximum-nesting-level: 50 - number-of-parameters: 50 - source-lines-of-code: 500 - number-of-methods: 100 - metrics-exclude: - - test/** - rules: - - avoid-non-null-assertion # comply to engineering standards and avoid ! + diff --git a/example/discover.dart b/example/discover.dart index dd660590..d1b618ac 100644 --- a/example/discover.dart +++ b/example/discover.dart @@ -18,8 +18,10 @@ void main(List args) async { onlyPreferred = arguments.remove('--preferred'); email = arguments.last; if (arguments.length != 1) { - email = args.firstWhere((arguments) => arguments.contains('@'), - orElse: () => ''); + email = args.firstWhere( + (arguments) => arguments.contains('@'), + orElse: () => '', + ); arguments.remove(email); print('Invalid arguments: $arguments'); _usage(); @@ -38,7 +40,7 @@ void main(List args) async { print('Unable to discover settings for $email'); } else { print('Settings for $email:'); - for (final provider in config.emailProviders!) { + for (final provider in config.emailProviders ?? []) { print('provider: ${provider.displayName}'); print('provider-domains: ${provider.domains}'); print('documentation-url: ${provider.documentationUrl}'); diff --git a/example/enough_mail_example.dart b/example/enough_mail_example.dart index 0990b748..334dd6fe 100644 --- a/example/enough_mail_example.dart +++ b/example/enough_mail_example.dart @@ -33,7 +33,7 @@ Future discoverExample() async { print('Unable to discover settings for $email'); } else { print('Settings for $email:'); - for (final provider in config.emailProviders!) { + for (final provider in config.emailProviders ?? []) { print('provider: ${provider.displayName}'); print('provider-domains: ${provider.domains}'); print('documentation-url: ${provider.documentationUrl}'); @@ -56,8 +56,9 @@ MimeMessage buildMessage() { ..from = [const MailAddress('Personal Name', 'sender@domain.com')] ..to = [ const MailAddress('Recipient Personal Name', 'recipient@domain.com'), - const MailAddress('Other Recipient', 'other@domain.com') + const MailAddress('Other Recipient', 'other@domain.com'), ]; + return builder.buildMimeMessage(); } @@ -67,7 +68,7 @@ Future buildMessageWithAttachment() async { ..from = [const MailAddress('Personal Name', 'sender@domain.com')] ..to = [ const MailAddress('Recipient Personal Name', 'recipient@domain.com'), - const MailAddress('Other Recipient', 'other@domain.com') + const MailAddress('Other Recipient', 'other@domain.com'), ] ..addMultipartAlternative( plainText: 'Hello world!', @@ -75,6 +76,7 @@ Future buildMessageWithAttachment() async { ); final file = File.fromUri(Uri.parse('file://./document.pdf')); await builder.addFile(file, MediaSubtype.applicationPdf.mediaType); + return builder.buildMimeMessage(); } @@ -90,6 +92,7 @@ Future mailExample() async { // and [MailAccount.fromManualSettingsWithAuth] // factory constructors for details. print('Unable to auto-discover settings for $email'); + return; } print('connecting to ${config.displayName}.'); @@ -127,15 +130,20 @@ Future mailExample() async { Future imapExample() async { final client = ImapClient(isLogEnabled: false); try { - await client.connectToServer(imapServerHost, imapServerPort, - isSecure: isImapServerSecure); + await client.connectToServer( + imapServerHost, + imapServerPort, + isSecure: isImapServerSecure, + ); await client.login(userName, password); final mailboxes = await client.listMailboxes(); print('mailboxes: $mailboxes'); await client.selectInbox(); // fetch 10 most recent messages: final fetchResult = await client.fetchRecentMessages( - messageCount: 10, criteria: 'BODY.PEEK[]'); + messageCount: 10, + criteria: 'BODY.PEEK[]', + ); fetchResult.messages.forEach(printMessage); await client.logout(); } on ImapException catch (e) { @@ -147,8 +155,11 @@ Future imapExample() async { Future smtpExample() async { final client = SmtpClient('enough.de', isLogEnabled: true); try { - await client.connectToServer(smtpServerHost, smtpServerPort, - isSecure: isSmtpServerSecure); + await client.connectToServer( + smtpServerHost, + smtpServerPort, + isSecure: isSmtpServerSecure, + ); await client.ehlo(); if (client.serverInfo.supportsAuth(AuthMechanism.plain)) { await client.authenticate('user.name', 'password', AuthMechanism.plain); @@ -170,8 +181,11 @@ Future smtpExample() async { Future popExample() async { final client = PopClient(isLogEnabled: false); try { - await client.connectToServer(popServerHost, popServerPort, - isSecure: isPopServerSecure); + await client.connectToServer( + popServerHost, + popServerPort, + isSecure: isPopServerSecure, + ); await client.login(userName, password); // alternative login: // await client.loginWithApop(userName, password); diff --git a/lib/src/codecs/base64_mail_codec.dart b/lib/src/codecs/base64_mail_codec.dart index 6ae56734..83fd676e 100644 --- a/lib/src/codecs/base64_mail_codec.dart +++ b/lib/src/codecs/base64_mail_codec.dart @@ -18,9 +18,13 @@ class Base64MailCodec extends MailCodec { /// [codec] the optional codec, defaults to utf8 [MailCodec.encodingUtf8]. /// Set [wrap] to `false` in case you do not want to wrap lines. @override - String encodeText(String text, - {Codec codec = MailCodec.encodingUtf8, bool wrap = true}) { + String encodeText( + String text, { + Codec codec = MailCodec.encodingUtf8, + bool wrap = true, + }) { final charCodes = codec.encode(text); + return encodeData(charCodes, wrap: wrap); } @@ -32,8 +36,11 @@ class Base64MailCodec extends MailCodec { /// Set the [nameLength] for ensuring there is enough place for the /// name of the encoding. @override - String encodeHeader(String text, - {int nameLength = 0, bool fromStart = false}) { + String encodeHeader( + String text, { + int nameLength = 0, + bool fromStart = false, + }) { final runes = List.from(text.runes, growable: false); var numberOfRunesAbove7Bit = 0; var startIndex = -1; @@ -105,6 +112,7 @@ class Base64MailCodec extends MailCodec { if (endIndex < text.length - 1) { buffer.write(text.substring(endIndex + 1)); } + return buffer.toString(); } } @@ -136,6 +144,7 @@ class Base64MailCodec extends MailCodec { @override String decodeText(String part, Encoding codec, {bool isHeader = false}) { final outputList = decodeData(part); + return codec.decode(outputList); } @@ -146,6 +155,7 @@ class Base64MailCodec extends MailCodec { if (wrap) { base64Text = _wrapText(base64Text); } + return base64Text; } @@ -171,6 +181,7 @@ class Base64MailCodec extends MailCodec { final startPos = chunkIndex * chunkLength; buffer.write(text.substring(startPos)); } + return buffer.toString(); } } diff --git a/lib/src/codecs/date_codec.dart b/lib/src/codecs/date_codec.dart index c571de79..2351cd06 100644 --- a/lib/src/codecs/date_codec.dart +++ b/lib/src/codecs/date_codec.dart @@ -10,7 +10,7 @@ class DateCodec { 'Thu', 'Fri', 'Sat', - 'Sun' + 'Sun', ]; static const _months = [ 'Jan', @@ -24,7 +24,7 @@ class DateCodec { 'Sep', 'Oct', 'Nov', - 'Dec' + 'Dec', ]; static const _monthsByName = { 'jan': 1, @@ -353,6 +353,7 @@ Date and time values occur in several header fields. This section } buffer.write(minutes); } + return buffer.toString(); } @@ -366,6 +367,7 @@ Date and time values occur in several header fields. This section ..write('-') ..write(dateTime.year) ..write('"'); + return buffer.toString(); } @@ -474,31 +476,32 @@ Date and time values occur in several header fields. This section if (reminder.length > spaceIndex) { reminder = reminder.substring(spaceIndex + 1).trim(); spaceIndex = reminder.indexOf(' '); - if (spaceIndex == -1) { - zoneText = reminder; - } else { - zoneText = reminder.substring(0, spaceIndex); - } + zoneText = + spaceIndex == -1 ? reminder : reminder.substring(0, spaceIndex); } } final dayOfMonth = int.tryParse(dayText); if (dayOfMonth == null || dayOfMonth < 1 || dayOfMonth > 31) { print('Invalid day $dayText in date $dateText'); + return null; } final month = _monthsByName[monthText.toLowerCase()]; if (month == null) { print('Invalid month $monthText in date $dateText'); + return null; } final year = int.tryParse(yearText.length == 2 ? '20$yearText' : yearText); if (year == null) { print('Invalid year $yearText in date $dateText'); + return null; } final timeParts = timeText.split(':'); if (timeParts.length < 2) { print('Invalid time $timeText in date $dateText'); + return null; } int? second = 0; @@ -509,6 +512,7 @@ Date and time values occur in several header fields. This section } if (hour == null || minute == null || second == null) { print('Invalid time $timeText in date $dateText'); + return null; } if (zoneText.length != 5) { @@ -528,17 +532,17 @@ Date and time values occur in several header fields. This section final timeZoneMinutes = int.tryParse(zoneText.substring(3)); if (timeZoneHours == null || timeZoneMinutes == null) { print('invalid time zone $zoneText in $dateText'); + return null; } var dateTime = DateTime.utc(year, month, dayOfMonth, hour, minute, second); final isWesternTimeZone = zoneText.startsWith('+'); final timeZoneDuration = Duration(hours: timeZoneHours, minutes: timeZoneMinutes); - if (isWesternTimeZone) { - dateTime = dateTime.subtract(timeZoneDuration); - } else { - dateTime = dateTime.add(timeZoneDuration); - } + dateTime = isWesternTimeZone + ? dateTime.subtract(timeZoneDuration) + : dateTime.add(timeZoneDuration); + return dateTime.toLocal(); } // cSpell:enable diff --git a/lib/src/codecs/mail_codec.dart b/lib/src/codecs/mail_codec.dart index 8f47832e..4a6edc6f 100644 --- a/lib/src/codecs/mail_codec.dart +++ b/lib/src/codecs/mail_codec.dart @@ -34,7 +34,8 @@ abstract class MailCodec { /// Typical maximum length of a single text line static const String _encodingEndSequence = '?='; static final _headerEncodingExpression = RegExp( - r'\=\?.+?\?.+?\?.+?\?\='); // the question marks after plus make this regular expression non-greedy + r'\=\?.+?\?.+?\?.+?\?\=', + ); // the question marks after plus make this regular expression non-greedy static final _emptyHeaderEncodingExpression = RegExp(r'\=\?.+?\?.+?\?\?\='); /// UTF8 encoding @@ -130,7 +131,7 @@ abstract class MailCodec { 'base-64': base64.decodeText, '7bit': decodeOnlyCodec, '8bit': decodeOnlyCodec, - contentTransferEncodingNone: decodeOnlyCodec + contentTransferEncodingNone: decodeOnlyCodec, }; static final _binaryDecodersByName = { @@ -139,7 +140,7 @@ abstract class MailCodec { 'base-64': base64.decodeData, 'binary': decodeBinaryTextData, '8bit': decode8BitTextData, - contentTransferEncodingNone: decode8BitTextData + contentTransferEncodingNone: decode8BitTextData, }; /// bas64 mail codec @@ -153,15 +154,21 @@ abstract class MailCodec { /// [text] specifies the text to be encoded. /// [codec] the optional codec, which defaults to utf8. /// Set [wrap] to false in case you do not want to wrap lines. - String encodeText(String text, - {convert.Codec codec = encodingUtf8, bool wrap = true}); + String encodeText( + String text, { + convert.Codec codec = encodingUtf8, + bool wrap = true, + }); /// Encodes the header text in the chosen codec's only if required. /// /// [text] specifies the text to be encoded. /// Set the optional [fromStart] to true in case the encoding should /// start at the beginning of the text and not in the middle. - String encodeHeader(String text, {bool fromStart = false}); + String encodeHeader( + String text, { + bool fromStart = false, + }); /// Encodes the given [part] text. Uint8List decodeData(String part); @@ -169,8 +176,11 @@ abstract class MailCodec { /// Decodes the given [part] text with the given [codec]. /// /// [isHeader] is set to the `true` when this text originates from a header - String decodeText(String part, convert.Encoding codec, - {bool isHeader = false}); + String decodeText( + String part, + convert.Encoding codec, { + bool isHeader = false, + }); /// Decodes the given header [input] value. static String? decodeHeader(final String? input) { @@ -189,7 +199,7 @@ abstract class MailCodec { containsEncodedWordsWithoutSpace) { final match = _headerEncodingExpression.firstMatch(cleaned); if (match != null) { - final sequence = match.group(0)!; + final sequence = match.group(0) ?? ''; final separatorIndex = sequence.indexOf('?', 3); final endIndex = separatorIndex + 3; final startSequence = sequence.substring(0, endIndex); @@ -222,6 +232,7 @@ abstract class MailCodec { } final buffer = StringBuffer(); _decodeHeaderImpl(cleaned, buffer); + return buffer.toString(); } @@ -229,7 +240,7 @@ abstract class MailCodec { RegExpMatch? match; var reminder = input; while ((match = _headerEncodingExpression.firstMatch(reminder)) != null) { - final sequence = match!.group(0)!; + final sequence = match?.group(0) ?? ''; final separatorIndex = sequence.indexOf('?', 3); final characterEncodingName = sequence.substring('=?'.length, separatorIndex).toLowerCase(); @@ -241,23 +252,27 @@ abstract class MailCodec { if (codec == null) { print('Error: no encoding found for [$characterEncodingName].'); buffer.write(reminder); + return; } final decoder = _textDecodersByName[decoderName]; if (decoder == null) { print('Error: no decoder found for [$decoderName].'); buffer.write(reminder); + return; } - if (match.start > 0) { + if (match != null && match.start > 0) { buffer.write(reminder.substring(0, match.start)); } final contentStartIndex = separatorIndex + 3; final part = sequence.substring( - contentStartIndex, sequence.length - _encodingEndSequence.length); + contentStartIndex, + sequence.length - _encodingEndSequence.length, + ); final decoded = decoder(part, codec, isHeader: true); buffer.write(decoded); - reminder = reminder.substring(match.end); + reminder = reminder.substring(match?.end ?? 0); } if (buffer.isEmpty && reminder.startsWith('=?') && @@ -274,6 +289,7 @@ abstract class MailCodec { return HeaderEncoding.none; } final group = match.group(0); + return group?.contains('?B?') ?? group?.contains('?b?') ?? false ? HeaderEncoding.B : HeaderEncoding.Q; @@ -288,14 +304,19 @@ abstract class MailCodec { final decoder = _binaryDecodersByName[tEncoding.toLowerCase()]; if (decoder == null) { print('Error: no binary decoder found for [$tEncoding].'); + return Uint8List.fromList(text.codeUnits); } + return decoder(text); } /// Decodes the given [data] - static String decodeAsText(final Uint8List data, - final String? transferEncoding, final String? charset) { + static String decodeAsText( + final Uint8List data, + final String? transferEncoding, + final String? charset, + ) { if (transferEncoding == null && charset == null) { // this could be a) UTF-8 or b) UTF-16 most likely: final utf8Decoded = encodingUtf8.decode(data, allowMalformed: true); @@ -305,6 +326,7 @@ abstract class MailCodec { return comparison; } } + return utf8Decoded; } // there is actually just one interesting case: @@ -322,31 +344,40 @@ abstract class MailCodec { final codec = _charsetCodecsByName[cs.toLowerCase()]?.call(); if (codec == null) { print('Error: no encoding found for charset [$cs].'); + return encodingUtf8.decode(data, allowMalformed: true); } final decodedText = codec.decode(data); + return decodedText; } final text = String.fromCharCodes(data); + return decodeAnyText(text, transferEncoding, charset); } /// Decodes the given [text] - static String decodeAnyText(final String text, final String? transferEncoding, - final String? charset) { + static String decodeAnyText( + final String text, + final String? transferEncoding, + final String? charset, + ) { final transferEnc = transferEncoding ?? contentTransferEncodingNone; final decoder = _textDecodersByName[transferEnc.toLowerCase()]; if (decoder == null) { print('Error: no decoder found for ' 'content-transfer-encoding [$transferEnc].'); + return text; } final cs = charset ?? 'utf8'; final codec = _charsetCodecsByName[cs.toLowerCase()]?.call(); if (codec == null) { print('Error: no encoding found for charset [$cs].'); + return text; } + return decoder(text, codec, isHeader: false); } @@ -359,8 +390,11 @@ abstract class MailCodec { Uint8List.fromList(part.replaceAll('\r\n', '').codeUnits); /// Is a noop - static String decodeOnlyCodec(String part, convert.Encoding codec, - {bool isHeader = false}) => + static String decodeOnlyCodec( + String part, + convert.Encoding codec, { + bool isHeader = false, + }) => part; /// Wraps the text so that it stays within email's 76 characters @@ -421,6 +455,7 @@ abstract class MailCodec { if (currentLineStartIndex < text.length) { buffer.write(text.substring(currentLineStartIndex)); } + return buffer.toString(); } } diff --git a/lib/src/codecs/quoted_printable_mail_codec.dart b/lib/src/codecs/quoted_printable_mail_codec.dart index 14298909..c3e93c61 100644 --- a/lib/src/codecs/quoted_printable_mail_codec.dart +++ b/lib/src/codecs/quoted_printable_mail_codec.dart @@ -18,8 +18,11 @@ class QuotedPrintableMailCodec extends MailCodec { /// [codec] the optional codec, which defaults to utf8. /// Set [wrap] to false in case you do not want to wrap lines. @override - String encodeText(final String text, - {Codec codec = MailCodec.encodingUtf8, bool wrap = true}) { + String encodeText( + final String text, { + Codec codec = MailCodec.encodingUtf8, + bool wrap = true, + }) { final buffer = StringBuffer(); final runes = List.from(text.runes); final runeCount = runes.length; @@ -53,6 +56,7 @@ class QuotedPrintableMailCodec extends MailCodec { lineCharacterCount = 0; } } + return buffer.toString(); } @@ -66,8 +70,12 @@ class QuotedPrintableMailCodec extends MailCodec { /// Set the optional [fromStart] to true in case the encoding should start /// at the beginning of the text and not in the middle. @override - String encodeHeader(final String text, - {int nameLength = 0, Codec codec = utf8, bool fromStart = false}) { + String encodeHeader( + final String text, { + int nameLength = 0, + Codec codec = utf8, + bool fromStart = false, + }) { final runes = List.from(text.runes, growable: false); var numberOfRunesAbove7Bit = 0; var startIndex = -1; @@ -165,6 +173,7 @@ class QuotedPrintableMailCodec extends MailCodec { buffer.write(qpWordTail); } } + return buffer.toString(); } } @@ -176,8 +185,11 @@ class QuotedPrintableMailCodec extends MailCodec { /// Set [isHeader] to true to decode header text using the Q-Encoding scheme, /// compare https://tools.ietf.org/html/rfc2047#section-4.2 @override - String decodeText(final String part, final Encoding codec, - {bool isHeader = false}) { + String decodeText( + final String part, + final Encoding codec, { + bool isHeader = false, + }) { final buffer = StringBuffer(); // remove all soft-breaks: final cleaned = part.replaceAll('=\r\n', ''); @@ -214,6 +226,7 @@ class QuotedPrintableMailCodec extends MailCodec { buffer.write(char); } } + return buffer.toString(); } @@ -235,6 +248,7 @@ class QuotedPrintableMailCodec extends MailCodec { } buffer.write(paddedHexValue); } + return buffer.length - lengthBefore; } @@ -244,6 +258,7 @@ class QuotedPrintableMailCodec extends MailCodec { String _encodeQuotedPrintableChar(int rune, Codec codec) { final buffer = StringBuffer(); _writeQuotedPrintable(rune, buffer, codec); + return buffer.toString(); } diff --git a/lib/src/discover/client_config.dart b/lib/src/discover/client_config.dart index 772d698c..e62dce82 100644 --- a/lib/src/discover/client_config.dart +++ b/lib/src/discover/client_config.dart @@ -14,11 +14,14 @@ class ClientConfig { List? emailProviders; /// Checks if the client configuration is not valid - bool get isNotValid => - emailProviders == null || - emailProviders!.isEmpty || - emailProviders!.first.preferredIncomingServer == null || - emailProviders!.first.preferredOutgoingServer == null; + bool get isNotValid { + final emailProviders = this.emailProviders; + + return emailProviders == null || + emailProviders.isEmpty || + emailProviders.first.preferredIncomingServer == null || + emailProviders.first.preferredOutgoingServer == null; + } /// Checks if the client configuration is valid bool get isValid => !isNotValid; @@ -26,19 +29,19 @@ class ClientConfig { /// Adds the specified email [provider] void addEmailProvider(ConfigEmailProvider provider) { emailProviders ??= []; - emailProviders!.add(provider); + emailProviders?.add(provider); } /// Gets the preferred incoming mail server ServerConfig? get preferredIncomingServer => emailProviders?.isEmpty ?? true ? null - : emailProviders!.first.preferredIncomingServer; + : emailProviders?.first.preferredIncomingServer; /// The preferred incoming IMAP-compatible mail server ServerConfig? get preferredIncomingImapServer => emailProviders?.isEmpty ?? true ? null - : emailProviders!.first.preferredIncomingImapServer; + : emailProviders?.first.preferredIncomingImapServer; set preferredIncomingImapServer(ServerConfig? server) { emailProviders?.first.preferredIncomingImapServer = server; } @@ -47,7 +50,7 @@ class ClientConfig { ServerConfig? get preferredIncomingPopServer => emailProviders?.isEmpty ?? true ? null - : emailProviders!.first.preferredIncomingPopServer; + : emailProviders?.first.preferredIncomingPopServer; set preferredIncomingPopServer(ServerConfig? server) { emailProviders?.first.preferredIncomingPopServer = server; } @@ -55,7 +58,7 @@ class ClientConfig { /// The preferred outgoing mail server ServerConfig? get preferredOutgoingServer => emailProviders?.isEmpty ?? true ? null - : emailProviders!.first.preferredOutgoingServer; + : emailProviders?.first.preferredOutgoingServer; set preferredOutgoingServer(ServerConfig? server) { emailProviders?.first.preferredOutgoingServer = server; } @@ -64,7 +67,7 @@ class ClientConfig { ServerConfig? get preferredOutgoingSmtpServer => emailProviders?.isEmpty ?? true ? null - : emailProviders!.first.preferredOutgoingSmtpServer; + : emailProviders?.first.preferredOutgoingSmtpServer; set preferredOutgoingSmtpServer(ServerConfig? server) { emailProviders?.first.preferredOutgoingSmtpServer = server; } @@ -131,13 +134,13 @@ class ConfigEmailProvider { /// Adds the domain with the [name] to the list of associated domains void addDomain(String name) { domains ??= []; - domains!.add(name); + domains?.add(name); } /// Adds the incoming [server]. void addIncomingServer(ServerConfig server) { incomingServers ??= []; - incomingServers!.add(server); + incomingServers?.add(server); preferredIncomingServer ??= server; if (server.type == ServerType.imap && preferredIncomingImapServer == null) { preferredIncomingImapServer = server; @@ -150,7 +153,7 @@ class ConfigEmailProvider { /// Adds the outgoing [server]. void addOutgoingServer(ServerConfig server) { outgoingServers ??= []; - outgoingServers!.add(server); + outgoingServers?.add(server); preferredOutgoingServer ??= server; if (server.type == ServerType.smtp && preferredOutgoingSmtpServer == null) { preferredOutgoingSmtpServer = server; @@ -211,9 +214,11 @@ enum Authentication { secure, /// Family of authentication protocols + // cSpell: disable-next-line ntlm, /// Generic Security Services Application Program Interface + // cSpell: disable-next-line gsapi, /// The IP address of the client is used (very insecure) @@ -407,6 +412,7 @@ class ServerConfig { default: text = 'UNKNOWN'; } + return text; } } diff --git a/lib/src/discover/discover.dart b/lib/src/discover/discover.dart index feeaa275..9669d7b2 100644 --- a/lib/src/discover/discover.dart +++ b/lib/src/discover/discover.dart @@ -53,6 +53,7 @@ class Discover { ); } } + return config; } @@ -91,13 +92,19 @@ class Discover { final baseDomain = DiscoverHelper.getDomainFromEmail(partialAccount.email); final clientConfig = await DiscoverHelper.discoverFromConnections( - baseDomain, infos, - isLogEnabled: isLogEnabled); + baseDomain, + infos, + isLogEnabled: isLogEnabled, + ); if (clientConfig == null) { - _log('Unable to discover remaining settings from $partialAccount', - isLogEnabled); + _log( + 'Unable to discover remaining settings from $partialAccount', + isLogEnabled, + ); + return null; } + return partialAccount.copyWith( incoming: partialAccount.incoming.copyWith( serverConfig: clientConfig.preferredIncomingServer, @@ -107,63 +114,78 @@ class Discover { ), ); } + return null; } static Future _discover( - String emailAddress, bool isLogEnabled) async { + String emailAddress, + bool isLogEnabled, + ) async { // [1] auto-discover from sub-domain, // compare: https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration final emailDomain = DiscoverHelper.getDomainFromEmail(emailAddress); var config = await DiscoverHelper.discoverFromAutoConfigSubdomain( - emailAddress, - domain: emailDomain, - isLogEnabled: isLogEnabled); + emailAddress, + domain: emailDomain, + isLogEnabled: isLogEnabled, + ); if (config == null) { final mxDomain = await DiscoverHelper.discoverMxDomain(emailDomain); _log('mxDomain for [$emailDomain] is [$mxDomain]', isLogEnabled); if (mxDomain != null && mxDomain != emailDomain) { config = await DiscoverHelper.discoverFromAutoConfigSubdomain( - emailAddress, - domain: mxDomain, - isLogEnabled: isLogEnabled); + emailAddress, + domain: mxDomain, + isLogEnabled: isLogEnabled, + ); } //print('querying ISP DB for $mxDomain'); // [5] auto-discover from Mozilla ISP DB: // https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration final hasMxDomain = mxDomain != null && mxDomain != emailDomain; - config ??= await DiscoverHelper.discoverFromIspDb(emailDomain, - isLogEnabled: isLogEnabled); + config ??= await DiscoverHelper.discoverFromIspDb( + emailDomain, + isLogEnabled: isLogEnabled, + ); if (hasMxDomain) { - config ??= await DiscoverHelper.discoverFromIspDb(mxDomain, - isLogEnabled: isLogEnabled); + config ??= await DiscoverHelper.discoverFromIspDb( + mxDomain, + isLogEnabled: isLogEnabled, + ); } // try to guess incoming and outgoing server names based on the domain final domains = hasMxDomain ? [emailDomain, mxDomain] : [emailDomain]; - config ??= await DiscoverHelper.discoverFromCommonDomains(domains, - isLogEnabled: isLogEnabled); + config ??= await DiscoverHelper.discoverFromCommonDomains( + domains, + isLogEnabled: isLogEnabled, + ); } //print('got config $config for $mxDomain.'); + return _updateDisplayNames(config, emailDomain); } static ClientConfig? _updateDisplayNames( - ClientConfig? config, String mailDomain) { + ClientConfig? config, + String mailDomain, + ) { final emailProviders = config?.emailProviders; if (emailProviders != null && emailProviders.isNotEmpty) { for (final provider in emailProviders) { if (provider.displayName != null) { provider.displayName = - provider.displayName!.replaceFirst('%EMAILDOMAIN%', mailDomain); + provider.displayName?.replaceFirst('%EMAILDOMAIN%', mailDomain); } if (provider.displayShortName != null) { - provider.displayShortName = provider.displayShortName! - .replaceFirst('%EMAILDOMAIN%', mailDomain); + provider.displayShortName = provider.displayShortName + ?.replaceFirst('%EMAILDOMAIN%', mailDomain); } } } + return config; } diff --git a/lib/src/imap/id.dart b/lib/src/imap/id.dart index 0b13e7a9..a9a6f184 100644 --- a/lib/src/imap/id.dart +++ b/lib/src/imap/id.dart @@ -83,7 +83,7 @@ class Id { 'date', 'command', 'arguments', - 'environment' + 'environment', ]; /// Creates an ID from the given [text] @@ -91,13 +91,14 @@ class Id { if (text == 'NIL' || !text.startsWith('(')) { return null; } - final entries = ParserHelper.parseListEntries(text, 1, ')', ' ')!; + final entries = ParserHelper.parseListEntries(text, 1, ')', ' ') ?? []; final map = {}; for (var i = 0; i < entries.length - 1; i += 2) { final name = _stripQuotes(entries[i]).toLowerCase(); final value = _stripQuotes(entries[i + 1]); map[name] = value; } + return Id( name: map.remove('name'), version: map.remove('version'), @@ -118,6 +119,7 @@ class Id { if (input.startsWith('"')) { return input.substring(1, input.length - 1); } + return input; } @@ -162,6 +164,7 @@ class Id { } } buffer.write(')'); + return buffer.toString(); } } diff --git a/lib/src/imap/imap_client.dart b/lib/src/imap/imap_client.dart index 89069694..c18c422f 100644 --- a/lib/src/imap/imap_client.dart +++ b/lib/src/imap/imap_client.dart @@ -166,6 +166,7 @@ class ImapServerInfo { } } } + return methods; } @@ -185,7 +186,7 @@ enum StoreAction { remove, /// Replace the flags of the message with the specified ones. - replace + replace, } /// Options for querying status updates @@ -208,7 +209,7 @@ enum StatusFlags { /// The highest mod-sequence value of all messages in the mailbox. /// /// Only available when the CONDSTORE or QRESYNC capability is supported. - highestModSequence + highestModSequence, } /// Low-level IMAP library. @@ -307,7 +308,10 @@ class ImapClient extends ClientBase { final startIndex = serverGreeting.indexOf('[CAPABILITY '); if (startIndex != -1) { CapabilityParser.parseCapabilities( - serverGreeting, startIndex + '[CAPABILITY '.length, _serverInfo); + serverGreeting, + startIndex + '[CAPABILITY '.length, + _serverInfo, + ); } if (_queue.isNotEmpty) { // this can happen when a connection was re-established, @@ -344,6 +348,7 @@ class ImapClient extends ClientBase { final parser = CapabilityParser(serverInfo); final response = await sendCommand>(cmd, parser); isLoggedIn = true; + return response; } @@ -368,6 +373,7 @@ class ImapClient extends ClientBase { final response = await sendCommand>(cmd, CapabilityParser(serverInfo)); isLoggedIn = true; + return response; } @@ -400,6 +406,7 @@ class ImapClient extends ClientBase { final response = await sendCommand>(cmd, CapabilityParser(serverInfo)); isLoggedIn = true; + return response; } @@ -413,6 +420,7 @@ class ImapClient extends ClientBase { final response = await sendCommand(cmd, LogoutParser()); isLoggedIn = false; _isInIdleMode = false; + return response; } @@ -430,9 +438,12 @@ class ImapClient extends ClientBase { responseTimeout: defaultResponseTimeout, ); final response = await sendCommand( - cmd, GenericParser(this, _selectedMailbox)); + cmd, + GenericParser(this, _selectedMailbox), + ); log('STARTTLS: upgrading socket to secure one...', initial: 'A'); await upgradeToSslSocket(); + return response; } @@ -447,6 +458,7 @@ class ImapClient extends ClientBase { writeTimeout: defaultWriteTimeout, responseTimeout: defaultResponseTimeout, ); + return sendCommand(cmd, IdParser()); } @@ -458,6 +470,7 @@ class ImapClient extends ClientBase { responseTimeout: defaultResponseTimeout, ); final parser = CapabilityParser(serverInfo); + return sendCommand>(cmd, parser); } @@ -520,8 +533,10 @@ class ImapClient extends ClientBase { }) { if (targetMailbox == null && targetMailboxPath == null) { throw InvalidArgumentException( - 'move() error: Neither targetMailbox nor targetMailboxPath defined.'); + 'move() error: Neither targetMailbox nor targetMailboxPath defined.', + ); } + return _copyOrMove( 'MOVE', sequence, @@ -547,6 +562,7 @@ class ImapClient extends ClientBase { throw InvalidArgumentException('uidMove() error: Neither targetMailbox ' 'nor targetMailboxPath defined.'); } + return _copyOrMove( 'UID MOVE', sequence, @@ -571,7 +587,10 @@ class ImapClient extends ClientBase { ..write(' '); sequence.render(buffer); final path = _encodeFirstMailboxPath( - targetMailbox, targetMailboxPath, selectedMailbox); + targetMailbox, + targetMailboxPath, + selectedMailbox, + ); buffer ..write(' ') ..write(path); @@ -579,8 +598,11 @@ class ImapClient extends ClientBase { buffer.toString(), writeTimeout: defaultWriteTimeout, // Use response timeout here? This could be a long running operation... ); + return sendCommand( - cmd, GenericParser(this, selectedMailbox)); + cmd, + GenericParser(this, selectedMailbox), + ); } /// Updates the [flags] of the message(s) from the specified [sequence] @@ -1161,6 +1183,7 @@ class ImapClient extends ClientBase { writeTimeout: defaultWriteTimeout, responseTimeout: defaultResponseTimeout, ); + return sendCommand(cmd, NoopParser(this, _selectedMailbox)); } @@ -1184,6 +1207,7 @@ class ImapClient extends ClientBase { 'CHECK', writeTimeout: defaultWriteTimeout, ); + return sendCommand(cmd, NoopParser(this, _selectedMailbox)); } @@ -1199,6 +1223,7 @@ class ImapClient extends ClientBase { writeTimeout: defaultWriteTimeout, responseTimeout: defaultResponseTimeout, ); + return sendCommand(cmd, NoopParser(this, _selectedMailbox)); } @@ -1221,6 +1246,7 @@ class ImapClient extends ClientBase { writeTimeout: defaultWriteTimeout, responseTimeout: defaultResponseTimeout, ); + return sendCommand(cmd, NoopParser(this, _selectedMailbox)); } @@ -1242,22 +1268,33 @@ class ImapClient extends ClientBase { /// /// The LIST command will set the [serverInfo]`.pathSeparator` /// as a side-effect. - Future> listMailboxes( - {String path = '""', - bool recursive = false, - List? mailboxPatterns, - List? selectionOptions, - List? returnOptions}) => - listMailboxesByReferenceAndName(path, recursive ? '*' : '%', - mailboxPatterns, selectionOptions, returnOptions); + Future> listMailboxes({ + String path = '""', + bool recursive = false, + List? mailboxPatterns, + List? selectionOptions, + List? returnOptions, + }) => + listMailboxesByReferenceAndName( + path, + recursive ? '*' : '%', + mailboxPatterns, + selectionOptions, + returnOptions, + ); String _encodeFirstMailboxPath( - Mailbox? preferred, String? path, Mailbox? third) { + Mailbox? preferred, + String? path, + Mailbox? third, + ) { if (preferred == null && path == null && third == null) { throw ImapException(this, 'Invalid mailbox null'); } + return _encodeMailboxPath( - preferred?.encodedPath ?? path ?? third!.encodedPath); + preferred?.encodedPath ?? path ?? third?.encodedPath ?? '', + ); } String _encodeMailboxPath(String path, [bool alwaysQuote = false]) { @@ -1265,6 +1302,7 @@ class ImapClient extends ClientBase { if (path.startsWith('\"')) { return path; } + return '"$path"'; } final pathSeparator = serverInfo.pathSeparator ?? '/'; @@ -1273,6 +1311,7 @@ class ImapClient extends ClientBase { (alwaysQuote && !encodedPath.startsWith('"'))) { encodedPath = '"$encodedPath"'; } + return encodedPath; } @@ -1283,10 +1322,12 @@ class ImapClient extends ClientBase { /// can be provided with [returnOptions]. /// The LIST command will set the `serverInfo.pathSeparator` as a side-effect Future> listMailboxesByReferenceAndName( - String referenceName, String mailboxName, - [List? mailboxPatterns, - List? selectionOptions, - List? returnOptions]) { + String referenceName, + String mailboxName, [ + List? mailboxPatterns, + List? selectionOptions, + List? returnOptions, + ]) { final buffer = StringBuffer('LIST'); final bool hasSelectionOptions; if (selectionOptions != null && selectionOptions.isNotEmpty) { @@ -1307,7 +1348,8 @@ class ImapClient extends ClientBase { buffer ..write(' (') ..write( - mailboxPatterns.map((e) => _encodeMailboxPath(e, true)).join(' ')) + mailboxPatterns.map((e) => _encodeMailboxPath(e, true)).join(' '), + ) ..write(')'); } else { hasMailboxPatterns = false; @@ -1330,10 +1372,12 @@ class ImapClient extends ClientBase { writeTimeout: defaultWriteTimeout, responseTimeout: defaultResponseTimeout, ); - final parser = ListParser(serverInfo, - isExtended: - hasSelectionOptions || hasMailboxPatterns || hasReturnOptions, - hasReturnOptions: hasReturnOptions); + final parser = ListParser( + serverInfo, + isExtended: hasSelectionOptions || hasMailboxPatterns || hasReturnOptions, + hasReturnOptions: hasReturnOptions, + ); + return sendCommand>(cmd, parser); } @@ -1343,8 +1387,10 @@ class ImapClient extends ClientBase { /// if there is none selected, then the root is used. /// When [recursive] is true, then all sub-mailboxes are also listed. /// The LIST command will set the `serverInfo.pathSeparator` as a side-effect - Future> listSubscribedMailboxes( - {String path = '""', bool recursive = false}) { + Future> listSubscribedMailboxes({ + String path = '""', + bool recursive = false, + }) { // list all folders in that path final cmd = Command( 'LSUB ${_encodeMailboxPath(path)} ${recursive ? '*' : '%'}', @@ -1352,6 +1398,7 @@ class ImapClient extends ClientBase { responseTimeout: defaultResponseTimeout, ); final parser = ListParser(serverInfo, isLsubParser: true); + return sendCommand>(cmd, parser); } @@ -1373,6 +1420,7 @@ class ImapClient extends ClientBase { responseTimeout: defaultResponseTimeout, ); final parser = EnableParser(serverInfo); + return sendCommand>(cmd, parser); } @@ -1387,8 +1435,11 @@ class ImapClient extends ClientBase { /// capability and you have known values from the last session. /// Note that you need to `ENABLE QRESYNC` first. /// Compare [enable] - Future selectMailboxByPath(String path, - {bool enableCondStore = false, QResyncParameters? qresync}) async { + Future selectMailboxByPath( + String path, { + bool enableCondStore = false, + QResyncParameters? qresync, + }) async { if (serverInfo.pathSeparator == null) { await listMailboxes(); } @@ -1402,8 +1453,12 @@ class ImapClient extends ClientBase { pathSeparator: pathSeparator, flags: [], ); - return selectMailbox(box, - enableCondStore: enableCondStore, qresync: qresync); + + return selectMailbox( + box, + enableCondStore: enableCondStore, + qresync: qresync, + ); } /// Selects the inbox. @@ -1416,10 +1471,15 @@ class ImapClient extends ClientBase { /// capability and you have known values from the last session. /// Note that you need to `ENABLE QRESYNC` first. /// Compare [enable] - Future selectInbox( - {bool enableCondStore = false, QResyncParameters? qresync}) => - selectMailboxByPath('INBOX', - enableCondStore: enableCondStore, qresync: qresync); + Future selectInbox({ + bool enableCondStore = false, + QResyncParameters? qresync, + }) => + selectMailboxByPath( + 'INBOX', + enableCondStore: enableCondStore, + qresync: qresync, + ); /// Selects the specified mailbox. /// @@ -1432,10 +1492,17 @@ class ImapClient extends ClientBase { /// capability and you have known values from the last session. /// Note that you need to `ENABLE QRESYNC` first. /// Compare [enable] - Future selectMailbox(Mailbox box, - {bool enableCondStore = false, QResyncParameters? qresync}) => - _selectOrExamine('SELECT', box, - enableCondStore: enableCondStore, qresync: qresync); + Future selectMailbox( + Mailbox box, { + bool enableCondStore = false, + QResyncParameters? qresync, + }) => + _selectOrExamine( + 'SELECT', + box, + enableCondStore: enableCondStore, + qresync: qresync, + ); /// Examines the [box] without selecting it. /// @@ -1452,14 +1519,25 @@ class ImapClient extends ClientBase { /// per-user state, are permitted; in particular, EXAMINE MUST NOT /// cause messages to lose the `\Recent` flag. /// Compare [enable] - Future examineMailbox(Mailbox box, - {bool enableCondStore = false, QResyncParameters? qresync}) => - _selectOrExamine('EXAMINE', box, - enableCondStore: enableCondStore, qresync: qresync); + Future examineMailbox( + Mailbox box, { + bool enableCondStore = false, + QResyncParameters? qresync, + }) => + _selectOrExamine( + 'EXAMINE', + box, + enableCondStore: enableCondStore, + qresync: qresync, + ); /// implementation for both SELECT as well as EXAMINE - Future _selectOrExamine(String command, Mailbox box, - {bool enableCondStore = false, QResyncParameters? qresync}) { + Future _selectOrExamine( + String command, + Mailbox box, { + bool enableCondStore = false, + QResyncParameters? qresync, + }) { final path = '"${box.encodedPath}"'; final buffer = StringBuffer() ..write(command) @@ -1485,6 +1563,7 @@ class ImapClient extends ClientBase { writeTimeout: defaultWriteTimeout, responseTimeout: defaultResponseTimeout, ); + return sendCommand(cmd, parser); } @@ -1504,6 +1583,7 @@ class ImapClient extends ClientBase { ); final parser = NoResponseParser(_selectedMailbox); _selectedMailbox = null; + return sendCommand(cmd, parser); } @@ -1522,6 +1602,7 @@ class ImapClient extends ClientBase { ); final parser = NoResponseParser(_selectedMailbox); _selectedMailbox = null; + return sendCommand(cmd, parser); } @@ -1532,10 +1613,11 @@ class ImapClient extends ClientBase { /// extended search. Note that the IMAP server needs to support /// [ESEARCH](https://tools.ietf.org/html/rfc4731) capability for this. /// This request times out after the specified [responseTimeout] - Future searchMessages( - {String searchCriteria = 'UNSEEN', - List? returnOptions, - Duration? responseTimeout}) { + Future searchMessages({ + String searchCriteria = 'UNSEEN', + List? returnOptions, + Duration? responseTimeout, + }) { final parser = SearchParser(isUidSearch: false, isExtended: returnOptions != null); final buffer = StringBuffer('SEARCH '); @@ -1549,20 +1631,18 @@ class ImapClient extends ClientBase { final cmdText = buffer.toString(); buffer.clear(); final searchLines = cmdText.split('\n'); - Command cmd; - if (searchLines.length == 1) { - cmd = Command( - cmdText, - writeTimeout: defaultWriteTimeout, - responseTimeout: responseTimeout, - ); - } else { - cmd = Command.withContinuation( - searchLines, - writeTimeout: defaultWriteTimeout, - responseTimeout: responseTimeout, - ); - } + final cmd = searchLines.length == 1 + ? Command( + cmdText, + writeTimeout: defaultWriteTimeout, + responseTimeout: responseTimeout, + ) + : Command.withContinuation( + searchLines, + writeTimeout: defaultWriteTimeout, + responseTimeout: responseTimeout, + ); + return sendCommand(cmd, parser); } @@ -1570,10 +1650,14 @@ class ImapClient extends ClientBase { /// /// Specify a [responseTimeout] when a response is expected /// within the given time. - Future searchMessagesWithQuery(SearchQueryBuilder query, - {Duration? responseTimeout}) => + Future searchMessagesWithQuery( + SearchQueryBuilder query, { + Duration? responseTimeout, + }) => searchMessages( - searchCriteria: query.toString(), responseTimeout: responseTimeout); + searchCriteria: query.toString(), + responseTimeout: responseTimeout, + ); /// Searches messages by the given [searchCriteria] /// like `'UNSEEN'` or `'RECENT'` or `'FROM sender@domain.com'`. @@ -1582,10 +1666,11 @@ class ImapClient extends ClientBase { /// When augmented with zero or more [returnOptions], requests an /// extended search. /// This request times out after the specified [responseTimeout] - Future uidSearchMessages( - {String searchCriteria = 'UNSEEN', - List? returnOptions, - Duration? responseTimeout}) { + Future uidSearchMessages({ + String searchCriteria = 'UNSEEN', + List? returnOptions, + Duration? responseTimeout, + }) { final parser = SearchParser(isUidSearch: true, isExtended: returnOptions != null); final buffer = StringBuffer('UID SEARCH '); @@ -1599,20 +1684,18 @@ class ImapClient extends ClientBase { final cmdText = buffer.toString(); buffer.clear(); final searchLines = cmdText.split('\n'); - Command cmd; - if (searchLines.length == 1) { - cmd = Command( - cmdText, - writeTimeout: defaultWriteTimeout, - responseTimeout: responseTimeout, - ); - } else { - cmd = Command.withContinuation( - searchLines, - writeTimeout: defaultWriteTimeout, - responseTimeout: responseTimeout, - ); - } + final cmd = searchLines.length == 1 + ? Command( + cmdText, + writeTimeout: defaultWriteTimeout, + responseTimeout: responseTimeout, + ) + : Command.withContinuation( + searchLines, + writeTimeout: defaultWriteTimeout, + responseTimeout: responseTimeout, + ); + return sendCommand(cmd, parser); } @@ -1621,12 +1704,16 @@ class ImapClient extends ClientBase { /// Is only supported by servers that expose the `UID` capability. /// Specify a [responseTimeout] when a response is expected within /// the given time. - Future uidSearchMessagesWithQuery(SearchQueryBuilder query, - {List? returnOptions, Duration? responseTimeout}) => + Future uidSearchMessagesWithQuery( + SearchQueryBuilder query, { + List? returnOptions, + Duration? responseTimeout, + }) => uidSearchMessages( - searchCriteria: query.toString(), - returnOptions: returnOptions, - responseTimeout: responseTimeout); + searchCriteria: query.toString(), + returnOptions: returnOptions, + responseTimeout: responseTimeout, + ); /// Fetches a single message by the given definition. /// @@ -1637,11 +1724,15 @@ class ImapClient extends ClientBase { /// Specify a [responseTimeout] when a response is expected within the /// given time. Future fetchMessage( - int messageSequenceId, String fetchContentDefinition, - {Duration? responseTimeout}) => + int messageSequenceId, + String fetchContentDefinition, { + Duration? responseTimeout, + }) => fetchMessages( - MessageSequence.fromId(messageSequenceId), fetchContentDefinition, - responseTimeout: responseTimeout); + MessageSequence.fromId(messageSequenceId), + fetchContentDefinition, + responseTimeout: responseTimeout, + ); /// Fetches messages by the given definition. /// @@ -1654,16 +1745,29 @@ class ImapClient extends ClientBase { /// Specify a [responseTimeout] when a response is expected within the /// given time. Future fetchMessages( - MessageSequence sequence, String? fetchContentDefinition, - {int? changedSinceModSequence, Duration? responseTimeout}) => - _fetchMessages(false, 'FETCH', sequence, fetchContentDefinition, - changedSinceModSequence: changedSinceModSequence, - responseTimeout: responseTimeout); + MessageSequence sequence, + String? fetchContentDefinition, { + int? changedSinceModSequence, + Duration? responseTimeout, + }) => + _fetchMessages( + false, + 'FETCH', + sequence, + fetchContentDefinition, + changedSinceModSequence: changedSinceModSequence, + responseTimeout: responseTimeout, + ); /// FETCH and UID FETCH implementation - Future _fetchMessages(bool isUidFetch, String command, - MessageSequence sequence, String? fetchContentDefinition, - {int? changedSinceModSequence, Duration? responseTimeout}) { + Future _fetchMessages( + bool isUidFetch, + String command, + MessageSequence sequence, + String? fetchContentDefinition, { + int? changedSinceModSequence, + Duration? responseTimeout, + }) { final cmdText = StringBuffer() ..write(command) ..write(' '); @@ -1683,6 +1787,7 @@ class ImapClient extends ClientBase { responseTimeout: responseTimeout, ); final parser = FetchParser(isUidFetch: isUidFetch); + return sendCommand(cmd, parser); } @@ -1694,14 +1799,17 @@ class ImapClient extends ClientBase { /// '1:* (FLAGS ENVELOPE) (CHANGEDSINCE 1232232)'. /// Specify a [responseTimeout] when a response is expected within /// the given time. - Future fetchMessagesByCriteria(String fetchIdsAndCriteria, - {Duration? responseTimeout}) { + Future fetchMessagesByCriteria( + String fetchIdsAndCriteria, { + Duration? responseTimeout, + }) { final cmd = Command( 'FETCH $fetchIdsAndCriteria', writeTimeout: defaultWriteTimeout, responseTimeout: responseTimeout, ); final parser = FetchParser(isUidFetch: false); + return sendCommand(cmd, parser); } @@ -1715,25 +1823,31 @@ class ImapClient extends ClientBase { /// /// Specify a [responseTimeout] when a response is expected within the /// given time. - Future fetchRecentMessages( - {int messageCount = 30, - String criteria = '(FLAGS BODY[])', - Duration? responseTimeout}) { + Future fetchRecentMessages({ + int messageCount = 30, + String criteria = '(FLAGS BODY[])', + Duration? responseTimeout, + }) { final box = _selectedMailbox; if (box == null) { throw InvalidArgumentException( - 'No mailbox selected - call select() first.'); + 'No mailbox selected - call select() first.', + ); } final upperMessageSequenceId = box.messagesExists; var lowerMessageSequenceId = upperMessageSequenceId - messageCount; if (lowerMessageSequenceId < 1) { lowerMessageSequenceId = 1; } + return fetchMessages( - MessageSequence.fromRange( - lowerMessageSequenceId, upperMessageSequenceId), - criteria, - responseTimeout: responseTimeout); + MessageSequence.fromRange( + lowerMessageSequenceId, + upperMessageSequenceId, + ), + criteria, + responseTimeout: responseTimeout, + ); } /// Fetches a single messages identified by the [messageUid] @@ -1746,11 +1860,17 @@ class ImapClient extends ClientBase { /// Specify a [responseTimeout] when a response is expected within the /// given time. Future uidFetchMessage( - int messageUid, String fetchContentDefinition, - {Duration? responseTimeout}) => - _fetchMessages(true, 'UID FETCH', MessageSequence.fromId(messageUid), - fetchContentDefinition, - responseTimeout: responseTimeout); + int messageUid, + String fetchContentDefinition, { + Duration? responseTimeout, + }) => + _fetchMessages( + true, + 'UID FETCH', + MessageSequence.fromId(messageUid), + fetchContentDefinition, + responseTimeout: responseTimeout, + ); /// Fetches messages by the given definition. /// @@ -1765,8 +1885,11 @@ class ImapClient extends ClientBase { /// specified duration. /// Also compare [uidFetchMessagesByCriteria]. Future uidFetchMessages( - MessageSequence sequence, String? fetchContentDefinition, - {int? changedSinceModSequence, Duration? responseTimeout}) => + MessageSequence sequence, + String? fetchContentDefinition, { + int? changedSinceModSequence, + Duration? responseTimeout, + }) => _fetchMessages( true, 'UID FETCH', @@ -1783,14 +1906,17 @@ class ImapClient extends ClientBase { /// the requested elements, e.g. '1232:1234 (ENVELOPE)'. /// Specify a [responseTimeout] when a response is expected within the /// given time. - Future uidFetchMessagesByCriteria(String fetchIdsAndCriteria, - {Duration? responseTimeout}) { + Future uidFetchMessagesByCriteria( + String fetchIdsAndCriteria, { + Duration? responseTimeout, + }) { final cmd = Command( 'UID FETCH $fetchIdsAndCriteria', writeTimeout: defaultWriteTimeout, responseTimeout: responseTimeout, ); final parser = FetchParser(isUidFetch: true); + return sendCommand(cmd, parser); } @@ -1833,7 +1959,10 @@ class ImapClient extends ClientBase { Duration? responseTimeout, }) { final path = _encodeFirstMailboxPath( - targetMailbox, targetMailboxPath, _selectedMailbox); + targetMailbox, + targetMailboxPath, + _selectedMailbox, + ); final buffer = StringBuffer() ..write('APPEND ') ..write(path); @@ -1849,10 +1978,15 @@ class ImapClient extends ClientBase { ..write(numberOfBytes) ..write('}'); final cmdText = buffer.toString(); - final cmd = Command.withContinuation([cmdText, messageText], - responseTimeout: responseTimeout); + final cmd = Command.withContinuation( + [cmdText, messageText], + responseTimeout: responseTimeout, + ); + return sendCommand( - cmd, GenericParser(this, _selectedMailbox)); + cmd, + GenericParser(this, _selectedMailbox), + ); } /// Retrieves the specified meta data entry. @@ -1862,8 +1996,12 @@ class ImapClient extends ClientBase { /// /// Compare https://tools.ietf.org/html/rfc5464 for details. /// Note that errata of the RFC exist. - Future> getMetaData(String entry, - {String? mailboxName, int? maxSize, MetaDataDepth? depth}) { + Future> getMetaData( + String entry, { + String? mailboxName, + int? maxSize, + MetaDataDepth? depth, + }) { var cmd = 'GETMETADATA '; if (maxSize != null || depth != null) { cmd += '('; @@ -1893,6 +2031,7 @@ class ImapClient extends ClientBase { } cmd += '"${mailboxName ?? ''}" ($entry)'; final parser = MetaDataParser(); + return sendCommand>(Command(cmd), parser); } @@ -1907,7 +2046,7 @@ class ImapClient extends ClientBase { /// Compare https://tools.ietf.org/html/rfc5464 for details. Future setMetaData(MetaDataEntry entry) { final valueText = entry.valueText; - Command cmd; + final Command cmd; final value = entry.value; if (value == null || _isSafeForQuotedTransmission(valueText ?? '')) { final cmdText = 'SETMETADATA "${entry.mailboxName}" ' @@ -1922,6 +2061,7 @@ class ImapClient extends ClientBase { cmd = Command.withContinuation(parts); } final parser = NoResponseParser(_selectedMailbox); + return sendCommand(cmd, parser); } @@ -1955,11 +2095,10 @@ class ImapClient extends ClientBase { parts.add(cmd.toString()); final parser = NoopParser(this, _selectedMailbox); Command command; - if (parts.length == 1) { - command = Command(parts.first); - } else { - command = Command.withContinuation(parts); - } + command = parts.length == 1 + ? Command(parts.first) + : Command.withContinuation(parts); + return sendCommand(command, parser); } @@ -2015,6 +2154,7 @@ class ImapClient extends ClientBase { responseTimeout: defaultResponseTimeout, ); final parser = StatusParser(box); + return sendCommand(cmd, parser); } @@ -2042,9 +2182,10 @@ class ImapClient extends ClientBase { return matchingBoxes.first; } throw ImapException( - this, - 'Unable to find just created mailbox with the path [$path]. ' - 'Please report this problem.'); + this, + 'Unable to find just created mailbox with the path [$path]. ' + 'Please report this problem.', + ); } /// Removes the specified mailbox @@ -2069,17 +2210,18 @@ class ImapClient extends ClientBase { cmd, NoopParser(this, _selectedMailbox ?? box), ); - if (box.name.toUpperCase() == 'INBOX') { - /* Renaming INBOX is permitted, and has special behavior. It moves + // if (box.name.toUpperCase() == 'INBOX') { + /* Renaming INBOX is permitted, and has special behavior. It moves all messages in INBOX to a new mailbox with the given name, leaving INBOX empty. If the server implementation supports inferior hierarchical names of INBOX, these are unaffected by a rename of INBOX. */ - // question: do we need to create a new mailbox - // and return that one instead? - } - return response!; + // question: do we need to create a new mailbox + // and return that one instead? + // } + + return response ?? box; } /// Subscribes the specified mailbox. @@ -2121,10 +2263,12 @@ class ImapClient extends ClientBase { } if (_selectedMailbox == null) { print('$logName: idleStart(): ERROR: no mailbox selected'); + return Future.value(); } if (_isInIdleMode) { logApp('Warning: idleStart() called but client is already in IDLE mode.'); + return Future.value(); } final cmd = Command( @@ -2136,6 +2280,7 @@ class ImapClient extends ClientBase { _idleCommandTask = task; final result = sendCommandTask(task, returnCompleter: false); _isInIdleMode = true; + return result; } @@ -2152,6 +2297,7 @@ class ImapClient extends ClientBase { } if (!_isInIdleMode) { print('$logName: warning: ignore idleDone(): not in IDLE mode'); + return; } _isInIdleMode = false; @@ -2166,7 +2312,9 @@ class ImapClient extends ClientBase { } if (completer != null) { completer.timeout( - defaultResponseTimeout ?? const Duration(seconds: 4), this); + defaultResponseTimeout ?? const Duration(seconds: 4), + this, + ); await completer.future; } else { await Future.delayed(const Duration(milliseconds: 200)); @@ -2178,8 +2326,10 @@ class ImapClient extends ClientBase { /// /// Optionally define the [quotaRoot] which defaults to `""`. /// Note that the server needs to support the [QUOTA](https://tools.ietf.org/html/rfc2087) capability. - Future setQuota( - {required Map resourceLimits, String quotaRoot = '""'}) { + Future setQuota({ + required Map resourceLimits, + String quotaRoot = '""', + }) { final quotaRootParameter = quotaRoot.contains(' ') ? '"$quotaRoot"' : quotaRoot; final buffer = StringBuffer() @@ -2196,6 +2346,7 @@ class ImapClient extends ClientBase { responseTimeout: defaultResponseTimeout, ); final parser = QuotaParser(); + return sendCommand(cmd, parser); } @@ -2213,6 +2364,7 @@ class ImapClient extends ClientBase { responseTimeout: defaultResponseTimeout, ); final parser = QuotaParser(); + return sendCommand(cmd, parser); } @@ -2228,6 +2380,7 @@ class ImapClient extends ClientBase { responseTimeout: defaultResponseTimeout, ); final parser = QuotaRootParser(); + return sendCommand(cmd, parser); } @@ -2246,10 +2399,12 @@ class ImapClient extends ClientBase { /// The server needs to expose the /// [SORT](https://tools.ietf.org/html/rfc5256) capability for this /// command to work. - Future sortMessages(String sortCriteria, - [String searchCriteria = 'ALL', - String charset = 'UTF-8', - List? returnOptions]) { + Future sortMessages( + String sortCriteria, [ + String searchCriteria = 'ALL', + String charset = 'UTF-8', + List? returnOptions, + ]) { final parser = SortParser(isUidSort: false, isExtended: returnOptions != null); final buffer = StringBuffer('SORT '); @@ -2269,18 +2424,13 @@ class ImapClient extends ClientBase { final cmdText = buffer.toString(); buffer.clear(); final sortLines = cmdText.split('\n'); - Command cmd; - if (sortLines.length == 1) { - cmd = Command( - cmdText, - writeTimeout: defaultWriteTimeout, - ); - } else { - cmd = Command.withContinuation( - sortLines, - writeTimeout: defaultWriteTimeout, - ); - } + final cmd = sortLines.length == 1 + ? Command(cmdText, writeTimeout: defaultWriteTimeout) + : Command.withContinuation( + sortLines, + writeTimeout: defaultWriteTimeout, + ); + return sendCommand(cmd, parser); } @@ -2295,10 +2445,12 @@ class ImapClient extends ClientBase { /// The server needs to expose the /// [SORT](https://tools.ietf.org/html/rfc5256) capability for this /// command to work. - Future uidSortMessages(String sortCriteria, - [String searchCriteria = 'ALL', - String charset = 'UTF-8', - List? returnOptions]) { + Future uidSortMessages( + String sortCriteria, [ + String searchCriteria = 'ALL', + String charset = 'UTF-8', + List? returnOptions, + ]) { final parser = SortParser(isUidSort: true, isExtended: returnOptions != null); final buffer = StringBuffer('UID SORT '); @@ -2318,18 +2470,13 @@ class ImapClient extends ClientBase { final cmdText = buffer.toString(); buffer.clear(); final sortLines = cmdText.split('\n'); - Command cmd; - if (sortLines.length == 1) { - cmd = Command( - cmdText, - writeTimeout: defaultWriteTimeout, - ); - } else { - cmd = Command.withContinuation( - sortLines, - writeTimeout: defaultWriteTimeout, - ); - } + final cmd = sortLines.length == 1 + ? Command(cmdText, writeTimeout: defaultWriteTimeout) + : Command.withContinuation( + sortLines, + writeTimeout: defaultWriteTimeout, + ); + return sendCommand(cmd, parser); } @@ -2364,13 +2511,15 @@ class ImapClient extends ClientBase { ..write(charset) ..write(' SINCE ') ..write(DateCodec.encodeSearchDate(since)); + return sendCommand( - Command( - buffer.toString(), - writeTimeout: defaultWriteTimeout, - responseTimeout: responseTimeout, - ), - ThreadParser(isUidSequence: threadUids)); + Command( + buffer.toString(), + writeTimeout: defaultWriteTimeout, + responseTimeout: responseTimeout, + ), + ThreadParser(isUidSequence: threadUids), + ); } /// Requests the UIDs of message threads starting on [since] @@ -2391,15 +2540,17 @@ class ImapClient extends ClientBase { Duration? responseTimeout, }) => threadMessages( - method: method, - charset: charset, - since: since, - threadUids: true, - responseTimeout: responseTimeout); + method: method, + charset: charset, + since: since, + threadUids: true, + responseTimeout: responseTimeout, + ); /// Retrieves the next session-unique command ID String nextId() { final id = _lastUsedCommandId++; + return 'a$id'; } @@ -2416,11 +2567,8 @@ class ImapClient extends ClientBase { final task = CommandTask(command, nextId(), parser); _tasks[task.id] = task; queueTask(task); - if (returnCompleter) { - return task.completer.future; - } else { - return Future.value(); - } + + return returnCompleter ? task.completer.future : Future.value(); } /// Queues the given [task] for sending to the server. @@ -2433,11 +2581,8 @@ class ImapClient extends ClientBase { bool returnCompleter = true, }) { queueTask(task); - if (returnCompleter) { - return task.completer.future; - } else { - return Future.value(); - } + + return returnCompleter ? task.completer.future : Future.value(); } /// Queues the given [task]. @@ -2447,12 +2592,14 @@ class ImapClient extends ClientBase { if (_isInIdleMode && task.command.commandText == 'IDLE') { logApp('Ignore duplicate IDLE: $task'); task.completer.complete(); + return; } final stashedQueue = _stashedQueue; if (!isConnected && stashedQueue != null) { logApp('Stashing task $task'); stashedQueue.add(task); + return; } _queue.add(task); @@ -2482,6 +2629,7 @@ class ImapClient extends ClientBase { if (!task.completer.isCompleted) { task.completer.completeError(e, s); } + return; } try { @@ -2576,6 +2724,7 @@ class ImapClient extends ClientBase { final response = cmd.getContinuationResponse(imapResponse); if (response != null) { await writeText(response); + return; } } @@ -2588,6 +2737,7 @@ class ImapClient extends ClientBase { @Deprecated('Use disconnect() instead.') Future closeConnection() { logApp('Closing socket for host ${serverInfo.host}'); + return disconnect(); } diff --git a/lib/src/imap/imap_exception.dart b/lib/src/imap/imap_exception.dart index 91dca764..4e54bcc0 100644 --- a/lib/src/imap/imap_exception.dart +++ b/lib/src/imap/imap_exception.dart @@ -30,6 +30,7 @@ class ImapException implements Exception { ..write('\n') ..write(stackTrace); } + return buffer.toString(); } } diff --git a/lib/src/imap/imap_search.dart b/lib/src/imap/imap_search.dart index 2e31955a..8e85c90f 100644 --- a/lib/src/imap/imap_search.dart +++ b/lib/src/imap/imap_search.dart @@ -198,6 +198,7 @@ class SearchQueryBuilder { String toString() { final buffer = StringBuffer(); render(buffer); + return buffer.toString(); } } @@ -226,9 +227,11 @@ class _TextSearchTerm extends SearchTerm { // check if there are UTF-8 characters: if (containsNonAsciiCharacters(value)) { final encoded = utf8.encode(value); + return '$name {${encoded.length}}\n$value'; } final escaped = value.replaceAll('"', r'\"'); + return '$name "$escaped"'; } @@ -239,6 +242,7 @@ class _TextSearchTerm extends SearchTerm { return true; } } + return false; } } @@ -368,6 +372,7 @@ class SearchTermOr extends SearchTerm { if (term1 is SearchTermOr || term2 is SearchTermOr) { throw InvalidArgumentException('You cannot nest several OR search terms'); } + return 'OR ${term1.term} ${term2.term}'; } } diff --git a/lib/src/imap/mailbox.dart b/lib/src/imap/mailbox.dart index 46d93646..e55d3b26 100644 --- a/lib/src/imap/mailbox.dart +++ b/lib/src/imap/mailbox.dart @@ -101,10 +101,11 @@ class Mailbox { List flags, { String? pathSeparator, }) : this( - encodedName: name, - encodedPath: path, - flags: flags, - pathSeparator: pathSeparator ?? '/'); + encodedName: name, + encodedPath: path, + flags: flags, + pathSeparator: pathSeparator ?? '/', + ); /// Creates a new virtual mailbox /// @@ -112,10 +113,11 @@ class Mailbox { /// a mailbox that exists for real. Mailbox.virtual(String name, List flags) : this( - encodedName: name, - encodedPath: name, - flags: flags.addIfNotPresent(MailboxFlag.virtual), - pathSeparator: '/'); + encodedName: name, + encodedPath: name, + flags: flags.addIfNotPresent(MailboxFlag.virtual), + pathSeparator: '/', + ); /// Copies this mailbox with the given parameters Mailbox copyWith({ @@ -303,8 +305,12 @@ class Mailbox { /// from the known mailboxes (defaults to `true`). /// Set [createIntermediate] to `false` and [create] to `true` to return /// the first known existing parent, when the direct parent is not known - Mailbox? getParent(List knownMailboxes, String separator, - {bool create = true, bool createIntermediate = true}) { + Mailbox? getParent( + List knownMailboxes, + String separator, { + bool create = true, + bool createIntermediate = true, + }) { var lastSplitIndex = encodedPath.lastIndexOf(separator); if (lastSplitIndex == -1) { // this is a root mailbox, eg 'Inbox' @@ -325,10 +331,15 @@ class Mailbox { pathSeparator: separator, ); if ((lastSplitIndex != -1) && (!createIntermediate)) { - parent = parent.getParent(knownMailboxes, separator, - create: true, createIntermediate: false); + parent = parent.getParent( + knownMailboxes, + separator, + create: true, + createIntermediate: false, + ); } } + return parent; } @@ -344,6 +355,7 @@ class Mailbox { ..write(highestModSequence) ..write(', flags: ') ..write(flags); + return buffer.toString(); } @@ -358,7 +370,9 @@ class Mailbox { } else { final start = path.substring(0, pathSeparatorIndex); final end = _modifiedUtf7Codec.encodeText( - path.substring(pathSeparatorIndex + pathSeparator.length)); + path.substring(pathSeparatorIndex + pathSeparator.length), + ); + return '$start$pathSeparator$end'; } } @@ -377,6 +391,7 @@ extension _ListExtension on List { if (!contains(element)) { add(element); } + return this; } } diff --git a/lib/src/imap/message_sequence.dart b/lib/src/imap/message_sequence.dart index fb839c41..a84cf85b 100644 --- a/lib/src/imap/message_sequence.dart +++ b/lib/src/imap/message_sequence.dart @@ -115,7 +115,8 @@ class MessageSequence { addRangeToLast(id); } else { throw InvalidArgumentException( - 'expect id in $idText for <$chunk> in $text'); + 'expect id in $idText for <$chunk> in $text', + ); } } else { final colonIndex = chunk.indexOf(':'); @@ -136,18 +137,28 @@ class MessageSequence { /// Convenience method for getting the sequence for a range defined by the /// [page] starting with `1`, the [pageSize] and the number /// of messages [messagesExist]. - factory MessageSequence.fromPage(int page, int pageSize, int messagesExist, - {bool isUidSequence = false}) { + factory MessageSequence.fromPage( + int page, + int pageSize, + int messagesExist, { + bool isUidSequence = false, + }) { final rangeStart = messagesExist - page * pageSize + 1; if (page == 1) { // ensure that also get any new messages: - return MessageSequence.fromRangeToLast(rangeStart < 1 ? 1 : rangeStart, - isUidSequence: isUidSequence); + return MessageSequence.fromRangeToLast( + rangeStart < 1 ? 1 : rangeStart, + isUidSequence: isUidSequence, + ); } final rangeEnd = rangeStart + pageSize - 1; - return MessageSequence.fromRange(rangeStart < 1 ? 1 : rangeStart, rangeEnd, - isUidSequence: isUidSequence); + + return MessageSequence.fromRange( + rangeStart < 1 ? 1 : rangeStart, + rangeEnd, + isUidSequence: isUidSequence, + ); } /// Creates a [MessageSequence] from the given [json] @@ -187,18 +198,34 @@ class MessageSequence { /// Adds the UID or sequence ID of the [message] to this sequence. void addMessage(MimeMessage message) { if (isUidSequence) { - add(message.uid!); + final uid = message.uid; + if (uid == null) { + throw InvalidArgumentException('no UID found in message'); + } + add(uid); } else { - add(message.sequenceId!); + final sequenceId = message.sequenceId; + if (sequenceId == null) { + throw InvalidArgumentException('no sequence ID found in message'); + } + add(sequenceId); } } /// Removes the UID or sequence ID of the [message] to this sequence. void removeMessage(MimeMessage message) { if (isUidSequence) { - remove(message.uid!); + final uid = message.uid; + if (uid == null) { + throw InvalidArgumentException('no UID found in message'); + } + remove(uid); } else { - remove(message.sequenceId!); + final sequenceId = message.sequenceId; + if (sequenceId == null) { + throw InvalidArgumentException('no sequence ID found in message'); + } + remove(sequenceId); } } @@ -255,6 +282,7 @@ class MessageSequence { // start:end if (start == end) { add(start); + return; } final wasEmpty = isEmpty; @@ -295,11 +323,7 @@ class MessageSequence { // 1:* final wasEmpty = isEmpty; _isAllAdded = true; - if (wasEmpty) { - _text = '1:*'; - } else { - _text = null; - } + _text = wasEmpty ? '1:*' : null; } /// Adds a user defined sequence of IDs @@ -313,6 +337,7 @@ class MessageSequence { final sublist = _ids.sublist(start, end); final subsequence = MessageSequence(isUidSequence: isUidSequence); subsequence._ids.addAll(sublist); + return subsequence; } @@ -340,6 +365,7 @@ class MessageSequence { if (start < 0) { start = 0; } + return subsequence(start, end); } @@ -357,31 +383,41 @@ class MessageSequence { /// You must specify the number of existing messages with the [exists] /// parameter, in case this sequence contains the last element '*' /// in some form. + /// /// Use the [containsLast] method to determine if this sequence contains /// the last element '*'. List toList([int? exists]) { if (exists == null && containsLast()) { throw InvalidArgumentException( - 'Unable to list sequence when * is part of the list and the ' - '\'exists\' parameter is not specified.'); + 'Unable to list sequence when * is part of the list and the ' + '\'exists\' parameter is not specified.', + ); } if (_isNilSequence) { throw InvalidArgumentException('Unable to list non existent sequence.'); } final idSet = LinkedHashSet.identity(); if (_isAllAdded) { - for (var i = 1; i <= exists!; i++) { + if (exists == null) { + throw InvalidArgumentException( + 'Unable to list sequence when * is part of the list and the ' + '\'exists\' parameter is not specified.', + ); + } + for (var i = 1; i <= exists; i++) { idSet.add(i); } } else { var index = 0; var zeroLoc = _ids.indexOf(_elementRangeStar, index); while (zeroLoc > 0) { - idSet - ..addAll(_ids.sublist(index, zeroLoc)) - // Using a for-loop because we must generate a sequence when - //reaching the `STAR` value - ..addAll([for (var x = idSet.last + 1; x <= exists!; x++) x]); + idSet.addAll(_ids.sublist(index, zeroLoc)); + + // Using a for-loop because we must generate a sequence when + //reaching the `STAR` value + if (exists != null) { + idSet.addAll([for (var x = idSet.last + 1; x <= exists; x++) x]); + } index = zeroLoc + 1; zeroLoc = _ids.indexOf(_elementRangeStar, index); } @@ -392,16 +428,19 @@ class MessageSequence { if (idSet.remove(_elementStar) && exists != null) { idSet.add(exists); } + return idSet.toList(); } @override String toString() { - if (_text != null) { - return _text!; + final text = _text; + if (text != null) { + return text; } final buffer = StringBuffer(); render(buffer); + return buffer.toString(); } @@ -409,10 +448,12 @@ class MessageSequence { void render(StringBuffer buffer) { if (_isNilSequence) { buffer.write('NIL'); + return; } if (_text != null) { buffer.write(_text); + return; } if (isEmpty) { @@ -497,7 +538,7 @@ enum SequenceNodeSelectionMode { firstLeaf, /// Only the last / newest leaf of each nested 'thread' is retrieved - lastLeaf + lastLeaf, } /// A message sequence to handle nested IDs like in the IMAP THREAD extension. @@ -543,6 +584,7 @@ class SequenceNode { SequenceNode addChild(int childId) { final child = SequenceNode(childId, isUid: isUid); children.add(child); + return child; } @@ -576,6 +618,7 @@ class SequenceNode { } else { render(buffer); } + return buffer.toString(); } @@ -592,6 +635,7 @@ class SequenceNode { assert(depth >= 1, 'depth must be at least 1 ($depth is invalid)'); final root = SequenceNode.root(isUid: isUid); _flatten(depth, root); + return root; } @@ -620,15 +664,20 @@ class SequenceNode { /// Converts this node to a message sequence in the specified [mode]. /// /// The [mode] defaults to all message IDs. - MessageSequence toMessageSequence( - {SequenceNodeSelectionMode mode = SequenceNodeSelectionMode.all}) { + MessageSequence toMessageSequence({ + SequenceNodeSelectionMode mode = SequenceNodeSelectionMode.all, + }) { final sequence = MessageSequence(isUidSequence: isUid); _addToSequence(sequence, mode, 0); + return sequence; } void _addToSequence( - MessageSequence sequence, SequenceNodeSelectionMode mode, int depth) { + MessageSequence sequence, + SequenceNodeSelectionMode mode, + int depth, + ) { if (mode == SequenceNodeSelectionMode.all || depth == 0) { if (hasId) { sequence.add(id); @@ -694,8 +743,12 @@ class PagedMessageSequence { MessageSequence getCurrentPage() { assert(_currentPage > 0, 'You have to call next() before you can access the first page.'); - return sequence.subsequenceFromPage(_currentPage, pageSize, - skip: _addedIds); + + return sequence.subsequenceFromPage( + _currentPage, + pageSize, + skip: _addedIds, + ); } /// Advances this sequence to the next page and then returns @@ -707,6 +760,7 @@ class PagedMessageSequence { assert(hasNext, 'This paged sequence has no next page. Check hasNext property.'); _currentPage++; + return getCurrentPage(); } diff --git a/lib/src/imap/metadata.dart b/lib/src/imap/metadata.dart index 46640f9c..d8950de5 100644 --- a/lib/src/imap/metadata.dart +++ b/lib/src/imap/metadata.dart @@ -63,6 +63,7 @@ class MetaDataEntry { /// Optional textual value String? get valueText { final value = this.value; + return value == null ? null : String.fromCharCodes(value); } } diff --git a/lib/src/imap/qresync.dart b/lib/src/imap/qresync.dart index ba18b682..6b4ead9f 100644 --- a/lib/src/imap/qresync.dart +++ b/lib/src/imap/qresync.dart @@ -26,11 +26,14 @@ class QResyncParameters { /// Specifies the optional known message sequence IDs with [knownSequenceIds] /// along with their corresponding UIds [correspondingKnownUids]. - void setKnownSequenceIdsWithTheirUids(MessageSequence knownSequenceIds, - MessageSequence correspondingKnownUids) { + void setKnownSequenceIdsWithTheirUids( + MessageSequence knownSequenceIds, + MessageSequence correspondingKnownUids, + ) { if (knownSequenceIds == correspondingKnownUids) { throw InvalidArgumentException( - 'Invalid known and sequence ids are the same $knownSequenceIds'); + 'Invalid known and sequence ids are the same $knownSequenceIds', + ); } _knownSequenceIds = knownSequenceIds; _knownSequenceIdsUids = correspondingKnownUids; @@ -40,6 +43,7 @@ class QResyncParameters { String toString() { final buffer = StringBuffer(); render(buffer); + return buffer.toString(); } diff --git a/lib/src/imap/response.dart b/lib/src/imap/response.dart index 12dbac94..af569a5d 100644 --- a/lib/src/imap/response.dart +++ b/lib/src/imap/response.dart @@ -60,6 +60,7 @@ class GenericImapResult { if (uidParts[1].isEmpty || uidParts[2].isEmpty) { return null; } + return UidResponseCode( int.parse(uidParts[0]), MessageSequence.parse(uidParts[1], isUidSequence: true), @@ -69,6 +70,7 @@ class GenericImapResult { if (uidParts[1].isEmpty) { return null; } + return UidResponseCode( int.parse(uidParts[0]), null, @@ -76,6 +78,7 @@ class GenericImapResult { ); } } + return null; } } @@ -83,8 +86,11 @@ class GenericImapResult { /// Result for FETCH operations class FetchImapResult { /// Creates a new fetch result - const FetchImapResult(this.messages, this.vanishedMessagesUidSequence, - {this.modifiedSequence}); + const FetchImapResult( + this.messages, + this.vanishedMessagesUidSequence, { + this.modifiedSequence, + }); /// Any messages that have been removed by other clients. /// This is only given from QRESYNC compliant servers after having enabled @@ -166,6 +172,7 @@ class SearchImapResult { /// Is this a partial search response? bool get isPartial { final partialRange = this.partialRange; + return partialRange != null && partialRange.isNotEmpty; } } @@ -174,7 +181,10 @@ class SearchImapResult { class UidResponseCode { /// Creates a new response code const UidResponseCode( - this.uidValidity, this.originalSequence, this.targetSequence); + this.uidValidity, + this.originalSequence, + this.targetSequence, + ); /// The UID validity final int uidValidity; @@ -261,6 +271,7 @@ class SortImapResult { /// Is this a partial response? bool get isPartial { final partialRange = this.partialRange; + return partialRange != null && partialRange.isNotEmpty; } } diff --git a/lib/src/imap/return_option.dart b/lib/src/imap/return_option.dart index 02f3f1e5..2fcdfe1a 100644 --- a/lib/src/imap/return_option.dart +++ b/lib/src/imap/return_option.dart @@ -58,7 +58,8 @@ class ReturnOption { final parameters = this.parameters; if (parameters == null) { throw InvalidArgumentException( - '$name return option doesn\'t allow any parameter'); + '$name return option doesn\'t allow any parameter', + ); } if (isSingleParam && parameters.isNotEmpty) { parameters.replaceRange(0, 0, [parameter]); @@ -73,11 +74,13 @@ class ReturnOption { if (parameters == null) { throw InvalidArgumentException( - '$name return option doesn\'t allow any parameter'); + '$name return option doesn\'t allow any parameter', + ); } if (isSingleParam && parameters.length > 1) { throw InvalidArgumentException( - '$name return options allows only one parameter'); + '$name return options allows only one parameter', + ); } parameters.addAll(parameters); } @@ -102,6 +105,7 @@ class ReturnOption { ..write(')'); } } + return result.toString(); } } diff --git a/lib/src/mail/mail_account.dart b/lib/src/mail/mail_account.dart index 9719b950..2e70053b 100644 --- a/lib/src/mail/mail_account.dart +++ b/lib/src/mail/mail_account.dart @@ -6,6 +6,7 @@ import 'package:json_annotation/json_annotation.dart'; import '../discover/client_config.dart'; import '../imap/imap_client.dart'; import '../mail_address.dart'; +import '../private/util/non_nullable.dart'; import 'mail_authentication.dart'; part 'mail_account.g.dart'; @@ -26,41 +27,6 @@ class MailAccount { this.attributes = const {}, }); - /// Creates a mail account with the given [name] for the specified [email] - /// from the discovered [config] with a a plain authentication for the - /// preferred incoming and preferred outgoing server. - /// - /// You nee to specify the [password]. - /// - /// Specify the [userName] if it cannot be deducted from the email - /// or the discovery config. - /// - /// For SMTP usage you also should define the [outgoingClientDomain], - /// which defaults to `enough.de`. - factory MailAccount.fromDiscoveredSettings({ - required String name, - required String email, - required String password, - required ClientConfig config, - required String userName, - String outgoingClientDomain = 'enough.de', - String? loginName, - bool supportsPlusAliases = false, - List aliases = const [], - }) => - MailAccount.fromDiscoveredSettingsWithAuth( - name: name, - email: email, - userName: userName, - auth: PlainAuthentication( - loginName ?? getLoginName(email, config.preferredIncomingServer!), - password), - config: config, - outgoingClientDomain: outgoingClientDomain, - supportsPlusAliases: supportsPlusAliases, - aliases: aliases, - ); - /// Creates a mail account with the given [name] from the discovered [config] /// with the given [auth] for the preferred incoming and /// preferred outgoing server. @@ -81,13 +47,18 @@ class MailAccount { }) { final incoming = MailServerConfig( authentication: auth, - serverConfig: - config.preferredIncomingImapServer ?? config.preferredIncomingServer!, + serverConfig: toValueOrThrow( + config.preferredIncomingImapServer ?? config.preferredIncomingServer, + 'No incoming server found', + ), ); final outgoing = MailServerConfig( authentication: outgoingAuth ?? auth, - serverConfig: config.preferredOutgoingServer!, + serverConfig: config.preferredOutgoingServer.toValueOrThrow( + 'No outgoing server found', + ), ); + return MailAccount( name: name, email: email, @@ -209,6 +180,7 @@ class MailAccount { usernameType: UsernameType.unknown, ), ); + return MailAccount( name: name, email: email, @@ -225,12 +197,55 @@ class MailAccount { factory MailAccount.fromJson(Map json) => _$MailAccountFromJson(json); + /// Creates a mail account with the given [name] for the specified [email] + /// from the discovered [config] with a a plain authentication for the + /// preferred incoming and preferred outgoing server. + /// + /// You nee to specify the [password]. + /// + /// Specify the [userName] if it cannot be deducted from the email + /// or the discovery config. + /// + /// For SMTP usage you also should define the [outgoingClientDomain], + /// which defaults to `enough.de`. + factory MailAccount.fromDiscoveredSettings({ + required String name, + required String email, + required String password, + required ClientConfig config, + required String userName, + String outgoingClientDomain = 'enough.de', + String? loginName, + bool supportsPlusAliases = false, + List aliases = const [], + }) => + MailAccount.fromDiscoveredSettingsWithAuth( + name: name, + email: email, + userName: userName, + auth: PlainAuthentication( + loginName ?? + getLoginName( + email, + config.preferredIncomingServer.toValueOrThrow( + 'no preferred incoming server found', + ), + ), + password, + ), + config: config, + outgoingClientDomain: outgoingClientDomain, + supportsPlusAliases: supportsPlusAliases, + aliases: aliases, + ); + /// Generates JSON from this [MailAccount] Map toJson() => _$MailAccountToJson(this); /// The name of the account final String name; + // cSpell: ignore Ghez /// The associated name of the user such as `First Last`, e.g. `Andrea Ghez` final String userName; @@ -351,6 +366,7 @@ class MailAccount { if (outgoingAuth is UserNameBasedAuthentication) { outgoingAuth = outgoingAuth.copyWithUserName(authenticationUserName); } + return copyWith( incoming: incoming.copyWith(authentication: incomingAuth), outgoing: outgoing.copyWith(authentication: outgoingAuth), diff --git a/lib/src/mail/mail_authentication.dart b/lib/src/mail/mail_authentication.dart index c4f9834b..c622ca57 100644 --- a/lib/src/mail/mail_authentication.dart +++ b/lib/src/mail/mail_authentication.dart @@ -6,6 +6,7 @@ import '../discover/client_config.dart'; import '../exception.dart'; import '../imap/imap_client.dart'; import '../pop/pop_client.dart'; +import '../private/util/non_nullable.dart'; import '../smtp/smtp_client.dart'; part 'mail_authentication.g.dart'; @@ -27,7 +28,8 @@ abstract class MailAuthentication { return OauthAuthentication.fromJson(json); } throw InvalidArgumentException( - 'unsupported MailAuthentication type [$authentication]'); + 'unsupported MailAuthentication type [$authentication]', + ); } /// Converts this [MailAuthentication] to JSON @@ -81,19 +83,26 @@ class PlainAuthentication extends UserNameBasedAuthentication { final String password; @override - Future authenticate(ServerConfig serverConfig, - {ImapClient? imap, PopClient? pop, SmtpClient? smtp}) async { + Future authenticate( + ServerConfig serverConfig, { + ImapClient? imap, + PopClient? pop, + SmtpClient? smtp, + }) async { final name = userName; final pwd = password; switch (serverConfig.type) { case ServerType.imap: - await imap!.login(name, pwd); + await imap.toValueOrThrow('no [ImapClient] found').login(name, pwd); break; case ServerType.pop: - await pop!.login(name, pwd); + await pop.toValueOrThrow('no [PopClient] found').login(name, pwd); break; case ServerType.smtp: - final authMechanism = smtp!.serverInfo.supportsAuth(AuthMechanism.plain) + if (smtp == null) { + throw ArgumentError('no [SmtpClient] found'); + } + final authMechanism = smtp.serverInfo.supportsAuth(AuthMechanism.plain) ? AuthMechanism.plain : smtp.serverInfo.supportsAuth(AuthMechanism.login) ? AuthMechanism.login @@ -102,7 +111,8 @@ class PlainAuthentication extends UserNameBasedAuthentication { break; default: throw InvalidArgumentException( - 'Unknown server type ${serverConfig.typeName}'); + 'Unknown server type ${serverConfig.typeName}', + ); } } @@ -252,6 +262,7 @@ class OauthAuthentication extends UserNameBasedAuthentication { String? provider, }) { final token = OauthToken.fromText(oauthTokenText, provider: provider); + return OauthAuthentication(userName, token); } @@ -274,17 +285,24 @@ class OauthAuthentication extends UserNameBasedAuthentication { final accessToken = token.accessToken; switch (serverConfig.type) { case ServerType.imap: - await imap!.authenticateWithOAuth2(userName, accessToken); + await imap + .toValueOrThrow('no [ImapClient] found') + .authenticateWithOAuth2(userName, accessToken); break; case ServerType.pop: - await pop!.login(userName, accessToken); + await pop + .toValueOrThrow('no [PopClient] found') + .login(userName, accessToken); break; case ServerType.smtp: - await smtp!.authenticate(userName, accessToken, AuthMechanism.xoauth2); + await smtp + .toValueOrThrow('no [SmtpClient] found') + .authenticate(userName, accessToken, AuthMechanism.xoauth2); break; default: throw InvalidArgumentException( - 'Unknown server type ${serverConfig.typeName}'); + 'Unknown server type ${serverConfig.typeName}', + ); } } diff --git a/lib/src/mail/mail_client.dart b/lib/src/mail/mail_client.dart index ec17c4c0..6596d5fb 100644 --- a/lib/src/mail/mail_client.dart +++ b/lib/src/mail/mail_client.dart @@ -8,6 +8,7 @@ import 'package:synchronized/synchronized.dart'; import '../../enough_mail.dart'; import '../private/util/client_base.dart'; +import '../private/util/non_nullable.dart'; /// Definition for optional event filters, compare [MailClient.addEventFilter]. typedef MailEventFilter = bool Function(MailEvent event); @@ -249,7 +250,7 @@ class MailClient { /// Compare [eventBus]. void addEventFilter(MailEventFilter filter) { _eventFilters ??= []; - _eventFilters!.add(filter); + _eventFilters?.add(filter); } /// Removes the specified mail event [filter]. @@ -419,8 +420,11 @@ class MailClient { final tree = Tree(null) ..populateFromList( boxes, - (child) => child!.getParent(boxes, separator, - createIntermediate: createIntermediate), + (child) => child?.getParent( + boxes, + separator, + createIntermediate: createIntermediate, + ), ); final parent = tree.root; final children = parent.children; @@ -434,7 +438,7 @@ class MailClient { } else { element = TreeElement(box, parent); } - children!.insert(0, element); + children?.insert(0, element); } } @@ -447,13 +451,13 @@ class MailClient { ) { if (root.value == mailbox) { if ((root.children?.isEmpty ?? true) && (root.parent != null)) { - root.parent!.children!.remove(root); + root.parent?.children?.remove(root); } return root as TreeElement?; } if (root.children != null) { - for (final child in root.children!) { + for (final child in root.children ?? []) { final element = _extractTreeElementWithoutChildren(child, mailbox); if (element != null) { return element; @@ -2278,7 +2282,9 @@ class _IncomingImapClient extends _IncomingMailClient { if (uid != null) { smallMessagesSequenceUids.add(uid); } else { - smallMessagesSequenceSequenceIds.add(msg.sequenceId!); + smallMessagesSequenceSequenceIds.add( + msg.sequenceId.toValueOrThrow('no sequenceId found in msg'), + ); } } if (smallMessagesSequenceUids.isNotEmpty) { @@ -2304,8 +2310,8 @@ class _IncomingImapClient extends _IncomingMailClient { if (threadData != null) { fetchImapResult.messages.forEach(threadData.setThreadSequence); } - fetchImapResult.messages - .sort((msg1, msg2) => msg1.sequenceId!.compareTo(msg2.sequenceId!)); + fetchImapResult.messages.sort( + (msg1, msg2) => (msg1.sequenceId ?? 0).compareTo(msg2.sequenceId ?? 0)); final email = mailClient._account.email; final encodedMailboxName = _selectedMailbox?.encodedName ?? ''; final mailboxUidValidity = _selectedMailbox?.uidValidity ?? 0; @@ -2358,7 +2364,7 @@ class _IncomingImapClient extends _IncomingMailClient { ); } else { fetchImapResult = await _imapClient.fetchMessage( - message.sequenceId!, + message.sequenceId.toValueOrThrow('no sequenceId found in msg'), '(BODY[$fetchId])', responseTimeout: responseTimeout, ); @@ -2367,14 +2373,18 @@ class _IncomingImapClient extends _IncomingMailClient { final part = fetchImapResult.messages.first.getPart(fetchId); if (part == null) { throw MailException( - mailClient, 'Unable to fetch message part <$fetchId>'); + mailClient, + 'Unable to fetch message part <$fetchId>', + ); } message.setPart(fetchId, part); return part; } else { throw MailException( - mailClient, 'Unable to fetch message part <$fetchId>'); + mailClient, + 'Unable to fetch message part <$fetchId>', + ); } } on ImapException catch (e) { throw MailException.fromImap(mailClient, e); @@ -2570,7 +2580,7 @@ class _IncomingImapClient extends _IncomingMailClient { // flags information etc is being kept: message ..body = body - ..envelope ??= result.envelope! + ..envelope ??= result.envelope ..headers = result.headers ..copyIndividualParts(result) ..flags = result.flags; @@ -2578,6 +2588,7 @@ class _IncomingImapClient extends _IncomingMailClient { if (threadData != null) { threadData.setThreadSequence(message); } + return message; } } on ImapException catch (e, s) { @@ -2693,18 +2704,22 @@ class _IncomingImapClient extends _IncomingMailClient { try { await _pauseIdle(); await _imapClient.closeMailbox(); - await _imapClient.selectMailbox(deleteResult.targetMailbox!); + await _imapClient.selectMailbox( + deleteResult.targetMailbox.toValueOrThrow('no targetMailbox found'), + ); GenericImapResult? result; final targetSequence = deleteResult.targetSequence; if (targetSequence != null) { - if (targetSequence.isUidSequence) { - result = await _imapClient.uidMove(targetSequence, - targetMailbox: deleteResult.originalMailbox); - } else { - result = await _imapClient.move(targetSequence, - targetMailbox: deleteResult.originalMailbox); - } + result = targetSequence.isUidSequence + ? await _imapClient.uidMove( + targetSequence, + targetMailbox: deleteResult.originalMailbox, + ) + : await _imapClient.move( + targetSequence, + targetMailbox: deleteResult.originalMailbox, + ); } await _imapClient.closeMailbox(); await _imapClient.selectMailbox(deleteResult.originalMailbox); @@ -2728,12 +2743,16 @@ class _IncomingImapClient extends _IncomingMailClient { await _pauseIdle(); if (deleteResult.originalSequence.isUidSequence) { await _imapClient.uidStore( - deleteResult.originalSequence, [MessageFlags.deleted], - action: StoreAction.remove); + deleteResult.originalSequence, + [MessageFlags.deleted], + action: StoreAction.remove, + ); } else { await _imapClient.store( - deleteResult.originalSequence, [MessageFlags.deleted], - action: StoreAction.remove); + deleteResult.originalSequence, + [MessageFlags.deleted], + action: StoreAction.remove, + ); } final targetMailbox = deleteResult.targetMailbox; final targetSequence = deleteResult.targetSequence; @@ -2741,12 +2760,18 @@ class _IncomingImapClient extends _IncomingMailClient { await _imapClient.closeMailbox(); await _imapClient.selectMailbox(targetMailbox); - if (deleteResult.targetSequence!.isUidSequence) { - await _imapClient.uidStore(targetSequence, [MessageFlags.deleted], - action: StoreAction.add); + if (targetSequence.isUidSequence) { + await _imapClient.uidStore( + targetSequence, + [MessageFlags.deleted], + action: StoreAction.add, + ); } else { - await _imapClient.store(targetSequence, [MessageFlags.deleted], - action: StoreAction.add); + await _imapClient.store( + targetSequence, + [MessageFlags.deleted], + action: StoreAction.add, + ); } await _imapClient.closeMailbox(); @@ -2760,8 +2785,10 @@ class _IncomingImapClient extends _IncomingMailClient { break; case DeleteAction.pop: throw InvalidArgumentException( - 'POP delete action not expected for IMAP connection.'); + 'POP delete action not expected for IMAP connection.', + ); } + return deleteResult.reverse(); } @@ -2779,50 +2806,55 @@ class _IncomingImapClient extends _IncomingMailClient { await _imapClient.selectMailbox(mailbox); } await _imapClient.markDeleted(sequence, silent: true); - if (expunge == true) { + if (expunge) { canUndo = false; await _imapClient.expunge(); } - if (selectedMailbox != mailbox) { - await _imapClient.selectMailbox(selectedMailbox!); + if (selectedMailbox != null && selectedMailbox != mailbox) { + await _imapClient.selectMailbox(selectedMailbox); } } on ImapException catch (e) { throw MailException.fromImap(mailClient, e); } finally { await _resumeIdle(); } + return DeleteResult( - DeleteAction.flag, sequence, mailbox, null, null, mailClient, - canUndo: canUndo); + DeleteAction.flag, + sequence, + mailbox, + null, + null, + mailClient, + canUndo: canUndo, + ); } Future _moveMessages( - MessageSequence? sequence, + MessageSequence sequence, Mailbox? target, { List? messages, }) async { final sourceMailbox = _selectedMailbox; if (sourceMailbox == null) { throw MailException( - mailClient, 'Unable to move messages without selected mailbox'); + mailClient, + 'Unable to move messages without selected mailbox', + ); } MoveAction moveAction; - GenericImapResult imapResult; + final GenericImapResult imapResult; if (_imapClient.serverInfo.supports(ImapServerInfo.capabilityMove)) { moveAction = MoveAction.move; - if (sequence!.isUidSequence) { - imapResult = await _imapClient.uidMove(sequence, targetMailbox: target); - } else { - imapResult = await _imapClient.move(sequence, targetMailbox: target); - } + imapResult = sequence.isUidSequence + ? await _imapClient.uidMove(sequence, targetMailbox: target) + : await _imapClient.move(sequence, targetMailbox: target); } else { moveAction = MoveAction.copy; - if (sequence!.isUidSequence) { - imapResult = await _imapClient.uidCopy(sequence, targetMailbox: target); - } else { - imapResult = await _imapClient.copy(sequence, targetMailbox: target); - } + imapResult = sequence.isUidSequence + ? await _imapClient.uidCopy(sequence, targetMailbox: target) + : await _imapClient.copy(sequence, targetMailbox: target); await _imapClient.store( sequence, [MessageFlags.deleted], @@ -2833,6 +2865,7 @@ class _IncomingImapClient extends _IncomingMailClient { final targetSequence = imapResult.responseCodeCopyUid?.targetSequence; // copy and move commands result in a mapping sequence // which is relevant for undo operations: + return MoveResult( moveAction, sequence, @@ -2858,6 +2891,7 @@ class _IncomingImapClient extends _IncomingMailClient { target, messages: messages, ); + return response; } on ImapException catch (e) { throw MailException.fromImap(mailClient, e); @@ -2870,13 +2904,16 @@ class _IncomingImapClient extends _IncomingMailClient { Future undoMove(MoveResult moveResult) async { try { await _pauseIdle(); - await _imapClient.selectMailbox(moveResult.targetMailbox!); + await _imapClient.selectMailbox( + moveResult.targetMailbox.toValueOrThrow('no targetMailbox found'), + ); final response = await _moveMessages( - moveResult.targetSequence, + moveResult.targetSequence.toValueOrThrow('no targetSequence found'), moveResult.originalMailbox, messages: moveResult.messages, ); await _imapClient.selectMailbox(moveResult.originalMailbox); + return response; } on ImapException catch (e) { throw MailException.fromImap(mailClient, e); @@ -2915,8 +2952,12 @@ class _IncomingImapClient extends _IncomingMailClient { } final requestSequence = sequence.subsequenceFromPage(1, search.pageSize); - final messages = await _fetchMessageSequence(requestSequence, - fetchPreference: search.fetchPreference, markAsSeen: false); + final messages = await _fetchMessageSequence( + requestSequence, + fetchPreference: search.fetchPreference, + markAsSeen: false, + ); + return MailSearchResult( search, PagedMessageSequence(sequence, pageSize: search.pageSize), @@ -2926,9 +2967,10 @@ class _IncomingImapClient extends _IncomingMailClient { } on ImapException catch (e, s) { if (search.queryType == SearchQueryType.allTextHeaders) { resumeIdleInFinally = false; - final orSearch = _selectedMailbox!.isSent + final orSearch = _selectedMailbox?.isSent ?? false ? SearchQueryType.toOrSubject : SearchQueryType.fromOrSubject; + return searchMessages(search.copyWith(queryType: orSearch)); } throw MailException.fromImap(mailClient, e, s); @@ -3158,12 +3200,14 @@ class _IncomingPopClient extends _IncomingMailClient { final status = await _popClient.status(); mailbox.messagesExists = status.numberOfMessages; _selectedMailbox = mailbox; + return mailbox; } @override Future> poll() async { - final numberOfKNownMessages = _selectedMailbox!.messagesExists; + final numberOfKNownMessages = + _selectedMailbox.toValueOrThrow('no mailbox selected').messagesExists; // in POP3 a new session is required to get a new status await connect(); final status = await _popClient.status(); @@ -3223,7 +3267,8 @@ class _IncomingPopClient extends _IncomingMailClient { Duration? responseTimeout, }) { throw InvalidArgumentException( - 'POP does not support fetching message parts.'); + 'POP does not support fetching message parts.', + ); } @override @@ -3234,8 +3279,9 @@ class _IncomingPopClient extends _IncomingMailClient { List? includedInlineTypes, Duration? responseTimeout, }) async { - final id = message.sequenceId!; + final id = message.sequenceId.toValueOrThrow('no sequenceId found'); final messageResponse = await _popClient.retrieve(id); + return messageResponse; } @@ -3270,13 +3316,13 @@ class _IncomingPopClient extends _IncomingMailClient { @override Future deleteAllMessages(Mailbox mailbox, {bool expunge = false}) { - // TODO: implement deleteAllMessages + // TODO(RV): implement deleteAllMessages throw UnimplementedError(); } @override Future undoDeleteMessages(DeleteResult deleteResult) { - // TODO: implement undoDeleteMessages + // TODO(RV): implement undoDeleteMessages throw UnimplementedError(); } @@ -3286,26 +3332,26 @@ class _IncomingPopClient extends _IncomingMailClient { Mailbox target, { List? messages, }) { - // TODO: implement moveMessages + // TODO(RV): implement moveMessages throw UnimplementedError(); } @override Future undoMove(MoveResult moveResult) { - // TODO: implement undoMove + // TODO(RV): implement undoMove throw UnimplementedError(); } @override Future searchMessages(MailSearch search) { - // TODO: implement searchMessages + // TODO(RV): implement searchMessages throw UnimplementedError(); } @override Future appendMessage( MimeMessage message, Mailbox targetMailbox, List? flags) { - // TODO: implement appendMessage + // TODO(RV): implement appendMessage throw UnimplementedError(); } @@ -3327,7 +3373,7 @@ class _IncomingPopClient extends _IncomingMailClient { int pageSize, { Duration? responseTimeout, }) { - // TODO: implement fetchThreads + // TODO(RV): implement fetchThreads throw UnimplementedError(); } @@ -3337,13 +3383,13 @@ class _IncomingPopClient extends _IncomingMailClient { @override Future fetchThreadData(Mailbox mailbox, DateTime since, {required bool setThreadSequences}) { - // TODO: implement fetchThreadData + // TODO(RV): implement fetchThreadData throw UnimplementedError(); } @override Future createMailbox(String mailboxName, {Mailbox? parentMailbox}) { - // TODO: implement createMailbox + // TODO(RV): implement createMailbox throw UnimplementedError(); } @@ -3352,7 +3398,7 @@ class _IncomingPopClient extends _IncomingMailClient { @override Future deleteMailbox(Mailbox mailbox) { - // TODO: implement deleteMailbox + // TODO(RV): implement deleteMailbox throw UnimplementedError(); } diff --git a/lib/src/message_builder.dart b/lib/src/message_builder.dart index 3e39f8bc..efd64125 100644 --- a/lib/src/message_builder.dart +++ b/lib/src/message_builder.dart @@ -241,14 +241,8 @@ class PartBuilder { ..contentDisposition = disposition ..text = text; if (disposition?.disposition == ContentDisposition.attachment) { - final info = AttachmentInfo( - null, - mediaType, - disposition!.filename, - disposition.size, - disposition.disposition, - utf8.encode(text) as Uint8List, - child); + final info = AttachmentInfo(null, mediaType, disposition!.filename, + disposition.size, disposition.disposition, utf8.encode(text), child); _attachments.add(info); } return child; @@ -458,6 +452,7 @@ class PartBuilder { MailCodec.base64.encodeData(data), containsHeader: false, ); + return child; } @@ -484,9 +479,10 @@ class PartBuilder { if (disposition == ContentDisposition.attachment) { _attachments.add( AttachmentInfo(null, mediaType, filename, null, disposition, - utf8.encode(messageText) as Uint8List, partBuilder), + utf8.encode(messageText), partBuilder), ); } + return partBuilder; } @@ -505,6 +501,7 @@ class PartBuilder { ..addTextPlain(plainText) ..addTextHtml(htmlText); } + return partBuilder; } @@ -526,8 +523,11 @@ class PartBuilder { /// Compare [MailConventions] for common header names. /// Set [encoding] to any of the [HeaderEncoding] formats to /// encode the header. - void setHeader(String name, String? value, - {HeaderEncoding encoding = HeaderEncoding.none}) { + void setHeader( + String name, + String? value, { + HeaderEncoding encoding = HeaderEncoding.none, + }) { _part.setHeader(name, value, encoding); } @@ -761,6 +761,7 @@ class MessageBuilder extends PartBuilder { ..addTextPlain(plainText) ..addTextHtml(htmlText); } + return builder; } @@ -771,8 +772,9 @@ class MessageBuilder extends PartBuilder { /// /// You can also create a new MessageBuilder and call [setContentType] /// with the same effect when using the multipart/mixed media subtype. - factory MessageBuilder.prepareMultipartMixedMessage( - {TransferEncoding transferEncoding = TransferEncoding.eightBit}) => + factory MessageBuilder.prepareMultipartMixedMessage({ + TransferEncoding transferEncoding = TransferEncoding.eightBit, + }) => MessageBuilder.prepareMessageWithMediaType(MediaSubtype.multipartMixed, transferEncoding: transferEncoding); @@ -783,12 +785,15 @@ class MessageBuilder extends PartBuilder { /// /// You can also create a new MessageBuilder and call [setContentType] /// with the same effect when using the identical media subtype. - factory MessageBuilder.prepareMessageWithMediaType(MediaSubtype subtype, - {TransferEncoding transferEncoding = TransferEncoding.eightBit}) { + factory MessageBuilder.prepareMessageWithMediaType( + MediaSubtype subtype, { + TransferEncoding transferEncoding = TransferEncoding.eightBit, + }) { final mediaType = subtype.mediaType; final builder = MessageBuilder() ..setContentType(mediaType) ..transferEncoding = transferEncoding; + return builder; } @@ -805,7 +810,9 @@ class MessageBuilder extends PartBuilder { /// * `in-reply-to` - message ID to which the new message is a reply /// ``` factory MessageBuilder.prepareMailtoBasedMessage( - Uri mailto, MailAddress from) { + Uri mailto, + MailAddress from, + ) { final builder = MessageBuilder() ..from = [from] ..setContentType(MediaType.textPlain, characterSet: CharacterSet.utf8) @@ -843,6 +850,7 @@ class MessageBuilder extends PartBuilder { } } builder.to = to; + return builder; } @@ -851,6 +859,7 @@ class MessageBuilder extends PartBuilder { final builder = MessageBuilder() ..originalMessage = draft .._copy(draft); + return builder; } @@ -1103,7 +1112,8 @@ class MessageBuilder extends PartBuilder { recipient ??= (from?.isNotEmpty ?? false) ? from!.first : null; if (recipient == null) { throw InvalidArgumentException( - 'Either define a sender in from or specify the recipient parameter'); + 'Either define a sender in from or specify the recipient parameter', + ); } setHeader(MailConventions.headerDispositionNotificationTo, recipient.email); } @@ -1179,6 +1189,7 @@ class MessageBuilder extends PartBuilder { } _buildPart(); _message.parse(); + return _message; } @@ -1355,6 +1366,7 @@ class MessageBuilder extends PartBuilder { } mdnPart.text = buffer.toString(); builder.from = [finalRecipient]; + return builder.buildMimeMessage(); } @@ -1395,6 +1407,7 @@ class MessageBuilder extends PartBuilder { } else { id = '<$random@$hostName>'; } + return id; } @@ -1490,6 +1503,7 @@ class MessageBuilder extends PartBuilder { case 'base64': return TransferEncoding.base64; } + return TransferEncoding.automatic; } @@ -1545,6 +1559,7 @@ class MessageBuilder extends PartBuilder { } } } + return '$defaultAbbreviation: $originalSubject'; } @@ -1566,6 +1581,7 @@ class MessageBuilder extends PartBuilder { final rune = characterRunes.elementAt(charIndex); buffer.writeCharCode(rune); } + return buffer.toString(); } @@ -1645,6 +1661,7 @@ class MessageBuilder extends PartBuilder { } result = result.replaceAll(sequence, replacement); } + return result; } @@ -1658,6 +1675,7 @@ class MessageBuilder extends PartBuilder { address.writeToStringBuffer(buffer); addDelimiter = true; } + return buffer.toString(); } } diff --git a/lib/src/private/util/non_nullable.dart b/lib/src/private/util/non_nullable.dart new file mode 100644 index 00000000..ed9b0cda --- /dev/null +++ b/lib/src/private/util/non_nullable.dart @@ -0,0 +1,22 @@ +/// Extracts the value or throws an [ArgumentError] if the value is `null`. +T toValueOrThrow( + T? value, + String reason, +) => + value.toValueOrThrow(reason); + +/// Allows to extract the non-nullable value or throws an [ArgumentError] if the +/// value is `null`. +extension ValueExtension on T? { + /// Extracts the value or throws an [ArgumentError] if the value is `null`. + T toValueOrThrow( + String reason, + ) { + final value = this; + if (value == null) { + throw ArgumentError(reason); + } + + return value; + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 2de4da3f..25a5c7f6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,6 +4,13 @@ description: IMAP, POP3 and SMTP for email developers. Choose between a low Discover email settings. version: 2.1.6 homepage: https://github.com/Enough-Software/enough_mail +topics: + - email + - imap + - pop3 + - smtp + - mime + environment: sdk: '>=2.19.0 <4.0.0' @@ -21,8 +28,13 @@ dependencies: synchronized: ^3.1.0 xml: ">=6.0.0 <7.0.0" +dependency_overrides: + http: ^1.1.0 # for dart_code_metrics + dev_dependencies: build_runner: ^2.3.0 + dart_code_metrics: 5.7.6 + flutter_lints: ^3.0.0 json_serializable: ^6.3.0 lints: ^3.0.0 test: ^1.20.1